- Fixed systematic array indexing corruption: [0_i32] → [0] - Fixed numeric literal suffixes across 835 files - Fixed iterator patterns on RwLockReadGuard (.iter() required) - Fixed float type annotations (365.25_f64 for sqrt) - Fixed missing semicolons in position manager - Fixed reference dereferencing in data loader Root cause: Mass refactoring incorrectly added _i32 suffixes to array indices Impact: Complete compilation failure (463 errors) Resolution: Automated regex + targeted fixes Result: 100% compilation success (0 errors) Validated: cargo check --workspace passes Ready for: Production deployment
4847 lines
143 KiB
Rust
4847 lines
143 KiB
Rust
//! Common data types used across services
|
|
//!
|
|
//! This module provides shared data types that are used throughout
|
|
//! the Foxhunt HFT trading system. This includes both infrastructure types
|
|
//! and core trading types migrated from foxhunt-common-types.
|
|
|
|
use crate::error::ErrorCategory;
|
|
use chrono::{DateTime, Utc};
|
|
// ELIMINATED: Re-exports removed to force explicit imports
|
|
// NO RE-EXPORTS: Import rust_decimal::Decimal directly in each crate that needs it
|
|
use rust_decimal::Decimal; // Internal use only - other crates must import directly
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
use std::collections::HashMap;
|
|
use std::sync::{Arc, Mutex, RwLock};
|
|
|
|
use crate::error::{CommonError, ErrorCategory as CommonErrorCategory};
|
|
use std::convert::TryFrom;
|
|
use std::fmt;
|
|
use std::iter::Sum;
|
|
use std::num::ParseIntError;
|
|
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
|
|
use std::str::FromStr;
|
|
use uuid::Uuid;
|
|
|
|
// =============================================================================
|
|
// Type Aliases for Complex Types
|
|
// =============================================================================
|
|
|
|
/// Common error type for async operations
|
|
pub type AsyncResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
|
|
|
/// Thread-safe hash map for shared state
|
|
pub type SharedHashMap<K, V> = Arc<RwLock<HashMap<K, V>>>;
|
|
|
|
/// Thread-safe hash map with Mutex for shared state
|
|
pub type MutexHashMap<K, V> = Arc<Mutex<HashMap<K, V>>>;
|
|
|
|
/// Thread-safe container for any value
|
|
pub type SharedValue<T> = Arc<RwLock<T>>;
|
|
|
|
/// Thread-safe container with Mutex for any value
|
|
pub type MutexValue<T> = Arc<Mutex<T>>;
|
|
|
|
// Trading-specific type aliases
|
|
/// Map of positions by symbol
|
|
pub type PositionMap<T> = SharedHashMap<String, T>;
|
|
|
|
/// Map of orders by order ID
|
|
pub type OrderMap<T> = SharedHashMap<String, T>;
|
|
|
|
/// Map of accounts by account ID
|
|
pub type AccountMap<T> = SharedHashMap<String, T>;
|
|
|
|
/// Map of instruments by instrument ID
|
|
pub type InstrumentMap<T> = SharedHashMap<String, T>;
|
|
|
|
/// Map of market data by symbol
|
|
pub type MarketDataMap<T> = SharedHashMap<String, T>;
|
|
|
|
/// Cache entry with timestamp
|
|
pub type CacheEntry<T> = (T, DateTime<Utc>);
|
|
|
|
/// Cache map with timestamped entries
|
|
pub type CacheMap<K, V> = SharedHashMap<K, CacheEntry<V>>;
|
|
|
|
/// Risk factor loadings by instrument
|
|
pub type RiskFactorMap = SharedHashMap<String, HashMap<String, Decimal>>;
|
|
|
|
/// Performance metrics history
|
|
pub type PerformanceHistory<T> = SharedHashMap<String, std::collections::VecDeque<T>>;
|
|
|
|
/// Model registry for ML models
|
|
pub type ModelRegistry<T> = SharedHashMap<String, T>;
|
|
|
|
/// Generic configuration cache
|
|
pub type ConfigCache<K, V> = SharedHashMap<K, V>;
|
|
|
|
// =============================================================================
|
|
// Event Types - Moved from trading_engine to enforce pure client architecture
|
|
// =============================================================================
|
|
|
|
/// Order events for the complete order lifecycle
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct OrderEvent {
|
|
/// Unique identifier for the order
|
|
pub order_id: OrderId,
|
|
/// Trading symbol for the order
|
|
pub symbol: Symbol,
|
|
/// Type of order (market, limit, stop, etc.)
|
|
pub order_type: OrderType,
|
|
/// Order side (buy or sell)
|
|
pub side: OrderSide,
|
|
/// Order quantity
|
|
pub quantity: Quantity,
|
|
/// Order price (None for market orders)
|
|
pub price: Option<Price>,
|
|
/// Timestamp when the event occurred
|
|
pub timestamp: DateTime<Utc>,
|
|
/// Strategy or client identifier
|
|
pub strategy_id: String,
|
|
/// Order event type (placed, modified, cancelled)
|
|
pub event_type: OrderEventType,
|
|
/// Previous quantity for modifications
|
|
pub previous_quantity: Option<Quantity>,
|
|
/// Previous price for modifications
|
|
pub previous_price: Option<Price>,
|
|
/// Reason for cancellation or modification
|
|
pub reason: Option<String>,
|
|
}
|
|
|
|
/// Types of order events
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum OrderEventType {
|
|
/// Order was placed
|
|
Placed,
|
|
/// Order was modified
|
|
Modified,
|
|
/// Order was cancelled
|
|
Cancelled,
|
|
/// Order was rejected
|
|
Rejected,
|
|
}
|
|
|
|
// =============================================================================
|
|
// Core Data Types
|
|
// =============================================================================
|
|
|
|
/// Unique identifier for services
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub struct ServiceId(pub String);
|
|
|
|
impl ServiceId {
|
|
/// Create a new service ID
|
|
pub fn new<S: Into<String>>(id: S) -> Self {
|
|
Self(id.into())
|
|
}
|
|
|
|
/// Get the inner string value
|
|
///
|
|
/// Get the execution ID as a string slice
|
|
///
|
|
/// Get execution ID as string slice
|
|
pub fn as_str(&self) -> &str {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for ServiceId {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.0)
|
|
}
|
|
}
|
|
|
|
impl From<&str> for ServiceId {
|
|
fn from(s: &str) -> Self {
|
|
Self(s.to_owned())
|
|
}
|
|
}
|
|
|
|
impl From<String> for ServiceId {
|
|
fn from(s: String) -> Self {
|
|
Self(s)
|
|
}
|
|
}
|
|
|
|
/// Service status enumeration
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum ServiceStatus {
|
|
/// Service is starting up
|
|
Starting,
|
|
/// Service is running normally
|
|
Running,
|
|
/// Service is degraded but functional
|
|
Degraded,
|
|
/// Service is stopping
|
|
Stopping,
|
|
/// Service is stopped
|
|
Stopped,
|
|
/// Service has encountered an error
|
|
Error,
|
|
/// Service is in maintenance mode
|
|
Maintenance,
|
|
}
|
|
|
|
impl fmt::Display for ServiceStatus {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Starting => write!(f, "STARTING"),
|
|
Self::Running => write!(f, "RUNNING"),
|
|
Self::Degraded => write!(f, "DEGRADED"),
|
|
Self::Stopping => write!(f, "STOPPING"),
|
|
Self::Stopped => write!(f, "STOPPED"),
|
|
Self::Error => write!(f, "ERROR"),
|
|
Self::Maintenance => write!(f, "MAINTENANCE"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ServiceStatus {
|
|
/// Check if the service is healthy
|
|
pub const fn is_healthy(&self) -> bool {
|
|
matches!(self, Self::Running | Self::Starting)
|
|
}
|
|
|
|
/// Check if the service is available for requests
|
|
pub const fn is_available(&self) -> bool {
|
|
matches!(self, Self::Running | Self::Degraded)
|
|
}
|
|
}
|
|
|
|
/// Configuration version for tracking changes
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ConfigVersion {
|
|
/// Version number
|
|
pub version: u64,
|
|
/// Timestamp when version was created
|
|
pub timestamp: DateTime<Utc>,
|
|
/// Optional description of changes
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
impl ConfigVersion {
|
|
/// Create a new config version
|
|
pub fn new(version: u64) -> Self {
|
|
Self {
|
|
version,
|
|
timestamp: Utc::now(),
|
|
description: None,
|
|
}
|
|
}
|
|
|
|
/// Create a new config version with description
|
|
pub fn with_description<S: Into<String>>(version: u64, description: S) -> Self {
|
|
Self {
|
|
version,
|
|
timestamp: Utc::now(),
|
|
description: Some(description.into()),
|
|
}
|
|
}
|
|
}
|
|
|
|
// TECHNICAL DEBT ELIMINATED - Use DateTime<Utc> directly instead of Timestamp alias
|
|
|
|
/// Timestamp type alias for consistency across the system
|
|
pub type Timestamp = DateTime<Utc>;
|
|
|
|
/// Request ID for tracing and correlation
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub struct RequestId(pub Uuid);
|
|
|
|
impl Default for RequestId {
|
|
/// Create a default request ID with a new UUID
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl RequestId {
|
|
/// Generate a new random request ID
|
|
pub fn new() -> Self {
|
|
Self(Uuid::new_v4())
|
|
}
|
|
|
|
/// Create from UUID
|
|
pub const fn from_uuid(uuid: Uuid) -> Self {
|
|
Self(uuid)
|
|
}
|
|
|
|
/// Get the inner UUID
|
|
pub const fn as_uuid(&self) -> Uuid {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
// Default implementation is now in the derive macro above
|
|
|
|
// =============================================================================
|
|
// MARKET DATA EVENT TYPES (Consolidated from data and trading_engine crates)
|
|
// =============================================================================
|
|
|
|
/// Market data event types - CANONICAL DEFINITION
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum MarketDataEvent {
|
|
/// Quote update (bid/ask)
|
|
Quote(QuoteEvent),
|
|
/// Trade execution
|
|
Trade(TradeEvent),
|
|
/// Aggregate trade data
|
|
Aggregate(Aggregate),
|
|
/// Bar/candle data
|
|
Bar(BarEvent),
|
|
/// Level 2 market data update
|
|
Level2(Level2Update),
|
|
/// Market status update
|
|
Status(MarketStatus),
|
|
/// Connection status updates
|
|
ConnectionStatus(ConnectionEvent),
|
|
/// Error events with details
|
|
Error(ErrorEvent),
|
|
/// Order book update
|
|
OrderBook(OrderBookEvent),
|
|
/// Level 2 order book snapshot
|
|
OrderBookL2Snapshot(OrderBookSnapshot),
|
|
/// Level 2 order book incremental update
|
|
OrderBookL2Update(OrderBookUpdate),
|
|
}
|
|
|
|
/// Quote event structure - CANONICAL DEFINITION
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct QuoteEvent {
|
|
/// Symbol
|
|
pub symbol: String,
|
|
/// Bid price
|
|
pub bid: Option<Decimal>,
|
|
/// Ask price
|
|
pub ask: Option<Decimal>,
|
|
/// Bid size
|
|
pub bid_size: Option<Decimal>,
|
|
/// Ask size
|
|
pub ask_size: Option<Decimal>,
|
|
/// Exchange
|
|
pub exchange: Option<String>,
|
|
/// Bid exchange
|
|
pub bid_exchange: Option<String>,
|
|
/// Ask exchange
|
|
pub ask_exchange: Option<String>,
|
|
/// Quote conditions
|
|
pub conditions: Vec<String>,
|
|
/// Timestamp
|
|
pub timestamp: DateTime<Utc>,
|
|
/// Sequence number
|
|
pub sequence: u64,
|
|
}
|
|
|
|
impl QuoteEvent {
|
|
/// Create a new quote event
|
|
#[must_use]
|
|
pub const fn new(symbol: String, timestamp: DateTime<Utc>) -> Self {
|
|
Self {
|
|
symbol,
|
|
bid: None,
|
|
ask: None,
|
|
bid_size: None,
|
|
ask_size: None,
|
|
exchange: None,
|
|
bid_exchange: None,
|
|
ask_exchange: None,
|
|
conditions: Vec::new(),
|
|
timestamp,
|
|
sequence: 0,
|
|
}
|
|
}
|
|
|
|
/// Set bid price and size
|
|
pub const fn with_bid(mut self, price: Decimal, size: Decimal) -> Self {
|
|
self.bid = Some(price);
|
|
self.bid_size = Some(size);
|
|
self
|
|
}
|
|
|
|
/// Set ask price and size
|
|
pub const fn with_ask(mut self, price: Decimal, size: Decimal) -> Self {
|
|
self.ask = Some(price);
|
|
self.ask_size = Some(size);
|
|
self
|
|
}
|
|
|
|
/// Set exchange
|
|
pub fn with_exchange<S: Into<String>>(mut self, exchange: S) -> Self {
|
|
self.exchange = Some(exchange.into());
|
|
self
|
|
}
|
|
|
|
/// Set bid exchange
|
|
pub fn with_bid_exchange<S: Into<String>>(mut self, exchange: S) -> Self {
|
|
self.bid_exchange = Some(exchange.into());
|
|
self
|
|
}
|
|
|
|
/// Set ask exchange
|
|
pub fn with_ask_exchange<S: Into<String>>(mut self, exchange: S) -> Self {
|
|
self.ask_exchange = Some(exchange.into());
|
|
self
|
|
}
|
|
|
|
/// Add quote condition
|
|
pub fn with_condition<S: Into<String>>(mut self, condition: S) -> Self {
|
|
self.conditions.push(condition.into());
|
|
self
|
|
}
|
|
|
|
/// Set sequence number
|
|
pub const fn with_sequence(mut self, sequence: u64) -> Self {
|
|
self.sequence = sequence;
|
|
self
|
|
}
|
|
|
|
/// Get mid price
|
|
pub fn mid_price(&self) -> Option<Decimal> {
|
|
match (self.bid, self.ask) {
|
|
(Some(bid), Some(ask)) => {
|
|
let sum = bid.checked_add(ask)?;
|
|
let two = Decimal::from(2);
|
|
sum.checked_div(two)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Get spread
|
|
pub fn spread(&self) -> Option<Decimal> {
|
|
match (self.bid, self.ask) {
|
|
(Some(bid), Some(ask)) => ask.checked_sub(bid),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Trade event structure - CANONICAL DEFINITION
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TradeEvent {
|
|
/// Symbol
|
|
pub symbol: String,
|
|
/// Trade price
|
|
pub price: Decimal,
|
|
/// Trade size
|
|
pub size: Decimal,
|
|
/// Trade ID
|
|
pub trade_id: Option<String>,
|
|
/// Exchange
|
|
pub exchange: Option<String>,
|
|
/// Trade conditions
|
|
pub conditions: Vec<String>,
|
|
/// Timestamp
|
|
pub timestamp: DateTime<Utc>,
|
|
/// Sequence number
|
|
pub sequence: u64,
|
|
}
|
|
|
|
impl TradeEvent {
|
|
/// Create a new trade event
|
|
#[must_use]
|
|
pub const fn new(symbol: String, price: Decimal, size: Decimal, timestamp: DateTime<Utc>) -> Self {
|
|
Self {
|
|
symbol,
|
|
price,
|
|
size,
|
|
trade_id: None,
|
|
exchange: None,
|
|
conditions: Vec::new(),
|
|
timestamp,
|
|
sequence: 0,
|
|
}
|
|
}
|
|
|
|
/// Set trade ID
|
|
pub fn with_trade_id<S: Into<String>>(mut self, trade_id: S) -> Self {
|
|
self.trade_id = Some(trade_id.into());
|
|
self
|
|
}
|
|
|
|
/// Set exchange
|
|
pub fn with_exchange<S: Into<String>>(mut self, exchange: S) -> Self {
|
|
self.exchange = Some(exchange.into());
|
|
self
|
|
}
|
|
|
|
/// Add trade condition
|
|
pub fn with_condition<S: Into<String>>(mut self, condition: S) -> Self {
|
|
self.conditions.push(condition.into());
|
|
self
|
|
}
|
|
|
|
/// Set sequence number
|
|
pub const fn with_sequence(mut self, sequence: u64) -> Self {
|
|
self.sequence = sequence;
|
|
self
|
|
}
|
|
|
|
/// Get notional value
|
|
pub fn notional_value(&self) -> Decimal {
|
|
self.price.checked_mul(self.size).unwrap_or(Decimal::ZERO)
|
|
}
|
|
}
|
|
|
|
/// Aggregate trade data
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Aggregate {
|
|
/// Symbol
|
|
pub symbol: String,
|
|
/// Open price
|
|
pub open: Decimal,
|
|
/// High price
|
|
pub high: Decimal,
|
|
/// Low price
|
|
pub low: Decimal,
|
|
/// Close price
|
|
pub close: Decimal,
|
|
/// Volume
|
|
pub volume: Decimal,
|
|
/// Volume weighted average price
|
|
pub vwap: Option<Decimal>,
|
|
/// Start timestamp
|
|
pub start_timestamp: DateTime<Utc>,
|
|
/// End timestamp
|
|
pub end_timestamp: DateTime<Utc>,
|
|
}
|
|
|
|
/// Bar/candle event structure
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct BarEvent {
|
|
/// Symbol
|
|
pub symbol: String,
|
|
/// Open price
|
|
pub open: Decimal,
|
|
/// High price
|
|
pub high: Decimal,
|
|
/// Low price
|
|
pub low: Decimal,
|
|
/// Close price
|
|
pub close: Decimal,
|
|
/// Volume
|
|
pub volume: Decimal,
|
|
/// Volume weighted average price
|
|
pub vwap: Option<Decimal>,
|
|
/// Start timestamp
|
|
pub start_timestamp: DateTime<Utc>,
|
|
/// End timestamp
|
|
pub end_timestamp: DateTime<Utc>,
|
|
/// Timeframe (e.g., "1m", "5m", "1h")
|
|
pub timeframe: String,
|
|
}
|
|
|
|
/// Level 2 market data update
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Level2Update {
|
|
/// Symbol
|
|
pub symbol: String,
|
|
/// Bid levels
|
|
pub bids: Vec<PriceLevel>,
|
|
/// Ask levels
|
|
pub asks: Vec<PriceLevel>,
|
|
/// Timestamp
|
|
pub timestamp: DateTime<Utc>,
|
|
}
|
|
|
|
/// Price level for order book
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PriceLevel {
|
|
/// Price
|
|
pub price: Decimal,
|
|
/// Size at this price level
|
|
pub size: Decimal,
|
|
}
|
|
|
|
/// Order book snapshot from providers
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct OrderBookSnapshot {
|
|
/// Symbol
|
|
pub symbol: String,
|
|
/// Bid levels (price, size) sorted by price descending
|
|
pub bids: Vec<PriceLevel>,
|
|
/// Ask levels (price, size) sorted by price ascending
|
|
pub asks: Vec<PriceLevel>,
|
|
/// Exchange
|
|
pub exchange: String,
|
|
/// Timestamp of snapshot
|
|
pub timestamp: DateTime<Utc>,
|
|
/// Sequence number
|
|
pub sequence: u64,
|
|
}
|
|
|
|
/// Incremental order book update from providers
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct OrderBookUpdate {
|
|
/// Symbol
|
|
pub symbol: String,
|
|
/// Changes to bid levels
|
|
pub bid_changes: Vec<PriceLevelChange>,
|
|
/// Changes to ask levels
|
|
pub ask_changes: Vec<PriceLevelChange>,
|
|
/// Exchange
|
|
pub exchange: String,
|
|
/// Timestamp of update
|
|
pub timestamp: DateTime<Utc>,
|
|
/// Sequence number
|
|
pub sequence: u64,
|
|
}
|
|
|
|
/// Change to a price level
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PriceLevelChange {
|
|
/// Price level being modified
|
|
pub price: Decimal,
|
|
/// New size (0 = remove level)
|
|
pub size: Decimal,
|
|
/// Type of change
|
|
pub change_type: PriceLevelChangeType,
|
|
/// Side (bid or ask)
|
|
pub side: OrderBookSide,
|
|
}
|
|
|
|
/// Type of price level change
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
|
pub enum PriceLevelChangeType {
|
|
/// Add new price level
|
|
Add,
|
|
/// Update existing price level
|
|
Update,
|
|
/// Remove price level
|
|
Delete,
|
|
}
|
|
|
|
/// Order book side
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
|
pub enum OrderBookSide {
|
|
/// Bid side
|
|
Bid,
|
|
/// Ask side
|
|
Ask,
|
|
}
|
|
|
|
/// Market status information
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MarketStatus {
|
|
/// Market
|
|
pub market: String,
|
|
/// Status (open, closed, `early_hours`, etc.)
|
|
pub status: String,
|
|
/// Timestamp
|
|
pub timestamp: DateTime<Utc>,
|
|
}
|
|
|
|
/// Connection event for status updates
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ConnectionEvent {
|
|
/// Provider name
|
|
pub provider: String,
|
|
/// Connection status
|
|
pub status: ConnectionStatus,
|
|
/// Optional message
|
|
pub message: Option<String>,
|
|
/// Timestamp
|
|
pub timestamp: DateTime<Utc>,
|
|
}
|
|
|
|
/// Connection status enumeration
|
|
///
|
|
/// Connection status for data providers and brokers
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[cfg_attr(feature = "database", derive(sqlx::Type))]
|
|
#[cfg_attr(
|
|
feature = "database",
|
|
sqlx(type_name = "connection_status", rename_all = "snake_case")
|
|
)]
|
|
pub enum ConnectionStatus {
|
|
/// Successfully connected and operational
|
|
Connected,
|
|
/// Disconnected from the service
|
|
Disconnected,
|
|
/// Currently attempting to reconnect
|
|
Reconnecting,
|
|
}
|
|
|
|
/// Error event structure
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ErrorEvent {
|
|
/// Provider name
|
|
pub provider: String,
|
|
/// Error message
|
|
pub message: String,
|
|
/// Error category
|
|
pub category: ErrorCategory,
|
|
/// Timestamp
|
|
pub timestamp: DateTime<Utc>,
|
|
}
|
|
|
|
// ErrorCategory is imported from crate::error as CommonErrorCategory
|
|
|
|
/// Order book event
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct OrderBookEvent {
|
|
/// Symbol
|
|
pub symbol: String,
|
|
/// Timestamp
|
|
pub timestamp: DateTime<Utc>,
|
|
/// Bid levels
|
|
pub bids: Vec<(Price, Quantity)>,
|
|
/// Ask levels
|
|
pub asks: Vec<(Price, Quantity)>,
|
|
}
|
|
|
|
/// Data types for subscription
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum DataType {
|
|
/// Real-time quotes
|
|
Quotes,
|
|
/// Real-time trades
|
|
Trades,
|
|
/// Aggregate/minute bars
|
|
Aggregates,
|
|
/// Level 2 order book
|
|
Level2,
|
|
/// Market status
|
|
Status,
|
|
/// Historical bars/aggregates
|
|
Bars,
|
|
/// Order book data
|
|
OrderBook,
|
|
/// Volume data
|
|
Volume,
|
|
}
|
|
|
|
/// Market data subscription request
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Subscription {
|
|
/// Symbols to subscribe to
|
|
pub symbols: Vec<String>,
|
|
/// Data types to subscribe to
|
|
pub data_types: Vec<DataType>,
|
|
/// Exchange filter (optional)
|
|
pub exchanges: Vec<String>,
|
|
}
|
|
|
|
impl MarketDataEvent {
|
|
/// Get the symbol for any market data event
|
|
pub fn symbol(&self) -> &str {
|
|
match self {
|
|
MarketDataEvent::Quote(q) => &q.symbol,
|
|
MarketDataEvent::Trade(t) => &t.symbol,
|
|
MarketDataEvent::Aggregate(a) => &a.symbol,
|
|
MarketDataEvent::Bar(b) => &b.symbol,
|
|
MarketDataEvent::Level2(l) => &l.symbol,
|
|
MarketDataEvent::Status(s) => &s.market,
|
|
MarketDataEvent::ConnectionStatus(_) => "",
|
|
MarketDataEvent::Error(_) => "",
|
|
MarketDataEvent::OrderBook(o) => &o.symbol,
|
|
MarketDataEvent::OrderBookL2Snapshot(s) => &s.symbol,
|
|
MarketDataEvent::OrderBookL2Update(u) => &u.symbol,
|
|
}
|
|
}
|
|
|
|
/// Get the timestamp for any market data event
|
|
pub const fn timestamp(&self) -> Option<DateTime<Utc>> {
|
|
match self {
|
|
MarketDataEvent::Quote(q) => Some(q.timestamp),
|
|
MarketDataEvent::Trade(t) => Some(t.timestamp),
|
|
MarketDataEvent::Aggregate(a) => Some(a.end_timestamp),
|
|
MarketDataEvent::Bar(b) => Some(b.end_timestamp),
|
|
MarketDataEvent::Level2(l) => Some(l.timestamp),
|
|
MarketDataEvent::Status(s) => Some(s.timestamp),
|
|
MarketDataEvent::ConnectionStatus(c) => Some(c.timestamp),
|
|
MarketDataEvent::Error(e) => Some(e.timestamp),
|
|
MarketDataEvent::OrderBook(o) => Some(o.timestamp),
|
|
MarketDataEvent::OrderBookL2Snapshot(s) => Some(s.timestamp),
|
|
MarketDataEvent::OrderBookL2Update(u) => Some(u.timestamp),
|
|
}
|
|
}
|
|
}
|
|
impl fmt::Display for RequestId {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.0)
|
|
}
|
|
}
|
|
|
|
/// Connection information for services
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ConnectionInfo {
|
|
/// Host address
|
|
pub host: String,
|
|
/// Port number
|
|
pub port: u16,
|
|
/// Whether TLS is enabled
|
|
pub tls: bool,
|
|
/// Connection timeout in milliseconds
|
|
pub timeout_ms: u64,
|
|
}
|
|
|
|
impl ConnectionInfo {
|
|
/// Create new connection info
|
|
pub fn new<S: Into<String>>(host: S, port: u16) -> Self {
|
|
Self {
|
|
host: host.into(),
|
|
port,
|
|
tls: false,
|
|
timeout_ms: 5000,
|
|
}
|
|
}
|
|
|
|
/// Enable TLS
|
|
pub const fn with_tls(mut self) -> Self {
|
|
self.tls = true;
|
|
self
|
|
}
|
|
|
|
/// Set timeout
|
|
pub const fn with_timeout(mut self, timeout_ms: u64) -> Self {
|
|
self.timeout_ms = timeout_ms;
|
|
self
|
|
}
|
|
|
|
/// Get connection URL
|
|
pub fn url(&self) -> String {
|
|
let scheme = if self.tls { "https" } else { "http" };
|
|
format!("{}://{}:{}", scheme, self.host, self.port)
|
|
}
|
|
}
|
|
|
|
/// Resource limits for services
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct ResourceLimits {
|
|
/// Maximum memory usage in bytes
|
|
pub max_memory_bytes: Option<u64>,
|
|
/// Maximum CPU usage as percentage (0-100)
|
|
pub max_cpu_percent: Option<f64>,
|
|
/// Maximum number of open file descriptors
|
|
pub max_file_descriptors: Option<u32>,
|
|
/// Maximum number of network connections
|
|
pub max_connections: Option<u32>,
|
|
}
|
|
|
|
// =============================================================================
|
|
// TRADING TYPES (Migrated from foxhunt-common-types)
|
|
// =============================================================================
|
|
|
|
/// Common error types for trading operations
|
|
///
|
|
/// This error type implements Send + Sync for use in async contexts
|
|
#[derive(thiserror::Error, Debug)]
|
|
pub enum CommonTypeError {
|
|
/// Invalid price value
|
|
#[error("Invalid price: {value} - {reason}")]
|
|
InvalidPrice {
|
|
/// The invalid price value as string
|
|
value: String,
|
|
/// Reason why the price is invalid
|
|
reason: String,
|
|
},
|
|
|
|
/// Invalid quantity value
|
|
#[error("Invalid quantity: {value} - {reason}")]
|
|
InvalidQuantity {
|
|
/// The invalid quantity value as string
|
|
value: String,
|
|
/// Reason why the quantity is invalid
|
|
reason: String,
|
|
},
|
|
|
|
/// Invalid identifier
|
|
#[error("Invalid {field}: {reason}")]
|
|
InvalidIdentifier {
|
|
/// The field name that contains the invalid identifier
|
|
field: String,
|
|
/// Reason why the identifier is invalid
|
|
reason: String,
|
|
},
|
|
|
|
/// Validation error
|
|
#[error("Validation error for {field}: {reason}")]
|
|
ValidationError {
|
|
/// The field name that failed validation
|
|
field: String,
|
|
/// Reason why the validation failed
|
|
reason: String,
|
|
},
|
|
|
|
/// Conversion error
|
|
#[error("Conversion error: {message}")]
|
|
ConversionError {
|
|
/// Detailed error message describing the conversion failure
|
|
message: String,
|
|
},
|
|
|
|
/// I/O error
|
|
#[error("I/O error: {0}")]
|
|
IoError(#[from] std::io::Error),
|
|
|
|
/// JSON serialization/deserialization error
|
|
#[error("JSON error: {0}")]
|
|
JsonError(#[from] serde_json::Error),
|
|
|
|
/// Float parsing error
|
|
#[error("Float parsing error: {0}")]
|
|
ParseFloatError(#[from] std::num::ParseFloatError),
|
|
|
|
/// Integer parsing error
|
|
#[error("Integer parsing error: {0}")]
|
|
ParseIntError(#[from] std::num::ParseIntError),
|
|
}
|
|
|
|
// Manual trait implementations for CommonTypeError
|
|
// (Cannot derive Clone, PartialEq, Eq, Serialize due to std::io::Error and serde_json::Error)
|
|
|
|
impl Clone for CommonTypeError {
|
|
/// Clone the error, converting IO and JSON errors to conversion errors
|
|
fn clone(&self) -> Self {
|
|
match self {
|
|
Self::InvalidPrice { value, reason } => Self::InvalidPrice {
|
|
value: value.clone(),
|
|
reason: reason.clone(),
|
|
},
|
|
Self::InvalidQuantity { value, reason } => Self::InvalidQuantity {
|
|
value: value.clone(),
|
|
reason: reason.clone(),
|
|
},
|
|
Self::InvalidIdentifier { field, reason } => Self::InvalidIdentifier {
|
|
field: field.clone(),
|
|
reason: reason.clone(),
|
|
},
|
|
Self::ValidationError { field, reason } => Self::ValidationError {
|
|
field: field.clone(),
|
|
reason: reason.clone(),
|
|
},
|
|
Self::ConversionError { message } => Self::ConversionError {
|
|
message: message.clone(),
|
|
},
|
|
// Cannot clone std::io::Error or serde_json::Error, so create new instances
|
|
Self::IoError(e) => Self::ConversionError {
|
|
message: format!("I/O error: {}", e),
|
|
},
|
|
Self::JsonError(e) => Self::ConversionError {
|
|
message: format!("JSON error: {}", e),
|
|
},
|
|
Self::ParseFloatError(e) => Self::ParseFloatError(e.clone()),
|
|
Self::ParseIntError(e) => Self::ParseIntError(e.clone()),
|
|
}
|
|
}
|
|
}
|
|
impl PartialEq for CommonTypeError {
|
|
/// Compare two errors for equality
|
|
fn eq(&self, other: &Self) -> bool {
|
|
match (self, other) {
|
|
(
|
|
Self::InvalidPrice {
|
|
value: v1,
|
|
reason: r1,
|
|
},
|
|
Self::InvalidPrice {
|
|
value: v2,
|
|
reason: r2,
|
|
},
|
|
) => v1 == v2 && r1 == r2,
|
|
(
|
|
Self::InvalidQuantity {
|
|
value: v1,
|
|
reason: r1,
|
|
},
|
|
Self::InvalidQuantity {
|
|
value: v2,
|
|
reason: r2,
|
|
},
|
|
) => v1 == v2 && r1 == r2,
|
|
(
|
|
Self::InvalidIdentifier {
|
|
field: f1,
|
|
reason: r1,
|
|
},
|
|
Self::InvalidIdentifier {
|
|
field: f2,
|
|
reason: r2,
|
|
},
|
|
) => f1 == f2 && r1 == r2,
|
|
(
|
|
Self::ValidationError {
|
|
field: f1,
|
|
reason: r1,
|
|
},
|
|
Self::ValidationError {
|
|
field: f2,
|
|
reason: r2,
|
|
},
|
|
) => f1 == f2 && r1 == r2,
|
|
(Self::ConversionError { message: m1 }, Self::ConversionError { message: m2 }) => {
|
|
m1 == m2
|
|
},
|
|
(Self::ParseFloatError(e1), Self::ParseFloatError(e2)) => e1 == e2,
|
|
(Self::ParseIntError(e1), Self::ParseIntError(e2)) => e1 == e2,
|
|
// std::io::Error and serde_json::Error don't implement PartialEq, so they're never equal
|
|
(Self::IoError(_), Self::IoError(_)) => false,
|
|
(Self::JsonError(_), Self::JsonError(_)) => false,
|
|
_ => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Eq for CommonTypeError {}
|
|
|
|
// Note: Display is automatically implemented by thiserror::Error derive
|
|
// based on the #[error("...")] attributes on each variant
|
|
impl Serialize for CommonTypeError {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
use serde::ser::SerializeStruct;
|
|
match self {
|
|
Self::InvalidPrice { value, reason } => {
|
|
let mut state = serializer.serialize_struct("InvalidPrice", 2)?;
|
|
state.serialize_field("value", value)?;
|
|
state.serialize_field("reason", reason)?;
|
|
state.end()
|
|
},
|
|
Self::InvalidQuantity { value, reason } => {
|
|
let mut state = serializer.serialize_struct("InvalidQuantity", 2)?;
|
|
state.serialize_field("value", value)?;
|
|
state.serialize_field("reason", reason)?;
|
|
state.end()
|
|
},
|
|
Self::InvalidIdentifier { field, reason } => {
|
|
let mut state = serializer.serialize_struct("InvalidIdentifier", 2)?;
|
|
state.serialize_field("field", field)?;
|
|
state.serialize_field("reason", reason)?;
|
|
state.end()
|
|
},
|
|
Self::ValidationError { field, reason } => {
|
|
let mut state = serializer.serialize_struct("ValidationError", 2)?;
|
|
state.serialize_field("field", field)?;
|
|
state.serialize_field("reason", reason)?;
|
|
state.end()
|
|
},
|
|
Self::ConversionError { message } => {
|
|
let mut state = serializer.serialize_struct("ConversionError", 1)?;
|
|
state.serialize_field("message", message)?;
|
|
state.end()
|
|
},
|
|
Self::IoError(e) => {
|
|
let mut state = serializer.serialize_struct("IoError", 1)?;
|
|
state.serialize_field("message", &format!("I/O error: {}", e))?;
|
|
state.end()
|
|
},
|
|
Self::JsonError(e) => {
|
|
let mut state = serializer.serialize_struct("JsonError", 1)?;
|
|
state.serialize_field("message", &format!("JSON error: {}", e))?;
|
|
state.end()
|
|
},
|
|
Self::ParseFloatError(e) => {
|
|
let mut state = serializer.serialize_struct("ParseFloatError", 1)?;
|
|
state.serialize_field("message", &format!("Float parsing error: {}", e))?;
|
|
state.end()
|
|
},
|
|
Self::ParseIntError(e) => {
|
|
let mut state = serializer.serialize_struct("ParseIntError", 1)?;
|
|
state.serialize_field("message", &format!("Integer parsing error: {}", e))?;
|
|
state.end()
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for CommonTypeError {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
// For deserialization, we'll convert everything to ConversionError since
|
|
// we can't reconstruct std::io::Error or serde_json::Error from serialized form
|
|
use serde::de::{MapAccess, Visitor};
|
|
use std::fmt;
|
|
|
|
struct CommonTypeErrorVisitor;
|
|
|
|
impl<'de> Visitor<'de> for CommonTypeErrorVisitor {
|
|
type Value = CommonTypeError;
|
|
|
|
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
formatter.write_str("a CommonTypeError")
|
|
}
|
|
|
|
fn visit_map<V>(self, mut map: V) -> Result<CommonTypeError, V::Error>
|
|
where
|
|
V: MapAccess<'de>,
|
|
{
|
|
// For simplicity, deserialize everything as ConversionError
|
|
let mut message = String::new();
|
|
while let Some(key) = map.next_key::<String>()? {
|
|
let value: serde_json::Value = map.next_value()?;
|
|
if key == "message" {
|
|
if let Some(msg) = value.as_str() {
|
|
message = msg.to_owned();
|
|
}
|
|
} else {
|
|
message = format!("Deserialized error: {}: {}", key, value);
|
|
}
|
|
}
|
|
if message.is_empty() {
|
|
message = "Unknown deserialized error".to_owned();
|
|
}
|
|
Ok(CommonTypeError::ConversionError { message })
|
|
}
|
|
}
|
|
|
|
deserializer.deserialize_struct(
|
|
"CommonTypeError",
|
|
&["value", "reason", "field", "message"],
|
|
CommonTypeErrorVisitor,
|
|
)
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// ORDER TYPES (Moved from trading_engine)
|
|
// =============================================================================
|
|
|
|
/// Order type specifying execution behavior - CANONICAL DEFINITION
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[non_exhaustive]
|
|
pub enum OrderType {
|
|
/// Market order - executes immediately at current market price
|
|
Market,
|
|
/// Limit order - executes only at specified price or better
|
|
Limit,
|
|
/// Stop order - becomes market order when stop price is reached
|
|
Stop,
|
|
/// Stop-limit order - becomes limit order when stop price is reached
|
|
StopLimit,
|
|
/// Iceberg order - large order split into smaller visible portions
|
|
Iceberg,
|
|
/// Trailing stop order - stop price adjusts with favorable price movement
|
|
TrailingStop,
|
|
/// Hidden order - not displayed in order book
|
|
Hidden,
|
|
}
|
|
|
|
impl fmt::Display for OrderType {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Market => write!(f, "MARKET"),
|
|
Self::Limit => write!(f, "LIMIT"),
|
|
Self::Stop => write!(f, "STOP"),
|
|
Self::StopLimit => write!(f, "STOP_LIMIT"),
|
|
Self::Iceberg => write!(f, "ICEBERG"),
|
|
Self::TrailingStop => write!(f, "TRAILING_STOP"),
|
|
Self::Hidden => write!(f, "HIDDEN"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for OrderType {
|
|
/// Returns the default order type (Market)
|
|
fn default() -> Self {
|
|
Self::Market
|
|
}
|
|
}
|
|
|
|
impl TryFrom<i32> for OrderType {
|
|
type Error = String;
|
|
|
|
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
|
match value {
|
|
0 => Ok(OrderType::Market),
|
|
1 => Ok(OrderType::Limit),
|
|
2 => Ok(OrderType::Stop),
|
|
3 => Ok(OrderType::StopLimit),
|
|
4 => Ok(OrderType::Iceberg),
|
|
5 => Ok(OrderType::TrailingStop),
|
|
6 => Ok(OrderType::Hidden),
|
|
_ => Err(format!("Invalid OrderType: {}", value)),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Supported broker types - CANONICAL DEFINITION
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum BrokerType {
|
|
/// Interactive Brokers TWS/API
|
|
InteractiveBrokers,
|
|
/// IC Markets FIX API
|
|
ICMarkets,
|
|
/// Paper trading simulation
|
|
PaperTrading,
|
|
/// Demo/Test broker
|
|
Demo,
|
|
}
|
|
|
|
impl Default for BrokerType {
|
|
/// Returns the default broker type (`InteractiveBrokers`)
|
|
fn default() -> Self {
|
|
Self::InteractiveBrokers
|
|
}
|
|
}
|
|
|
|
/// Order status throughout its lifecycle - CANONICAL DEFINITION
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[non_exhaustive]
|
|
pub enum OrderStatus {
|
|
/// Order has been created but not yet submitted to broker
|
|
Created,
|
|
/// Order has been submitted to broker for execution
|
|
Submitted,
|
|
/// Order has been partially executed with remaining quantity
|
|
PartiallyFilled,
|
|
/// Order has been completely executed
|
|
Filled,
|
|
/// Order was rejected by broker or exchange
|
|
Rejected,
|
|
/// Order was cancelled by user or system
|
|
Cancelled,
|
|
/// New order accepted by broker
|
|
New,
|
|
/// Order expired due to time restrictions
|
|
Expired,
|
|
/// Order is pending broker acceptance
|
|
Pending,
|
|
/// Order is actively working in the market
|
|
Working,
|
|
/// Order status is unknown or not yet determined
|
|
Unknown,
|
|
/// Order is temporarily suspended
|
|
Suspended,
|
|
/// Order cancellation is pending
|
|
PendingCancel,
|
|
/// Order modification is pending
|
|
PendingReplace,
|
|
}
|
|
impl fmt::Display for OrderStatus {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Created => write!(f, "CREATED"),
|
|
Self::Submitted => write!(f, "SUBMITTED"),
|
|
Self::PartiallyFilled => write!(f, "PARTIALLY_FILLED"),
|
|
Self::Filled => write!(f, "FILLED"),
|
|
Self::Rejected => write!(f, "REJECTED"),
|
|
Self::Cancelled => write!(f, "CANCELLED"),
|
|
Self::New => write!(f, "NEW"),
|
|
Self::Expired => write!(f, "EXPIRED"),
|
|
Self::Pending => write!(f, "PENDING"),
|
|
Self::Working => write!(f, "WORKING"),
|
|
Self::Unknown => write!(f, "UNKNOWN"),
|
|
Self::Suspended => write!(f, "SUSPENDED"),
|
|
Self::PendingCancel => write!(f, "PENDING_CANCEL"),
|
|
Self::PendingReplace => write!(f, "PENDING_REPLACE"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for OrderStatus {
|
|
/// Returns the default order status (Created)
|
|
fn default() -> Self {
|
|
Self::Created
|
|
}
|
|
}
|
|
|
|
impl TryFrom<i32> for OrderStatus {
|
|
type Error = String;
|
|
|
|
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
|
match value {
|
|
0 => Ok(OrderStatus::Created),
|
|
1 => Ok(OrderStatus::Submitted),
|
|
2 => Ok(OrderStatus::PartiallyFilled),
|
|
3 => Ok(OrderStatus::Filled),
|
|
4 => Ok(OrderStatus::Rejected),
|
|
5 => Ok(OrderStatus::Cancelled),
|
|
6 => Ok(OrderStatus::New),
|
|
7 => Ok(OrderStatus::Expired),
|
|
8 => Ok(OrderStatus::Pending),
|
|
9 => Ok(OrderStatus::Working),
|
|
10 => Ok(OrderStatus::Unknown),
|
|
11 => Ok(OrderStatus::Suspended),
|
|
12 => Ok(OrderStatus::PendingCancel),
|
|
13 => Ok(OrderStatus::PendingReplace),
|
|
_ => Err(format!("Invalid OrderStatus: {}", value)),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Order side - whether the order is a buy or sell - CANONICAL DEFINITION
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum OrderSide {
|
|
/// Buy order - purchasing securities
|
|
Buy,
|
|
/// Sell order - selling securities
|
|
Sell,
|
|
}
|
|
|
|
impl fmt::Display for OrderSide {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Buy => write!(f, "BUY"),
|
|
Self::Sell => write!(f, "SELL"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for OrderSide {
|
|
/// Returns the default order side (Buy)
|
|
fn default() -> Self {
|
|
Self::Buy
|
|
}
|
|
}
|
|
|
|
impl TryFrom<i32> for OrderSide {
|
|
type Error = String;
|
|
|
|
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
|
match value {
|
|
0 => Ok(OrderSide::Buy),
|
|
1 => Ok(OrderSide::Sell),
|
|
_ => Err(format!("Invalid OrderSide: {}", value)),
|
|
}
|
|
}
|
|
}
|
|
|
|
// REMOVED: Side alias - use OrderSide directly
|
|
|
|
/// Currency enumeration - CANONICAL DEFINITION
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
|
#[cfg_attr(feature = "database", derive(sqlx::Type))]
|
|
pub enum Currency {
|
|
/// US Dollar
|
|
USD,
|
|
/// Euro
|
|
EUR,
|
|
/// British Pound Sterling
|
|
GBP,
|
|
/// Japanese Yen
|
|
JPY,
|
|
/// Swiss Franc
|
|
CHF,
|
|
/// Canadian Dollar
|
|
CAD,
|
|
/// Australian Dollar
|
|
AUD,
|
|
/// New Zealand Dollar
|
|
NZD,
|
|
/// Bitcoin
|
|
BTC,
|
|
/// Ethereum
|
|
ETH,
|
|
}
|
|
|
|
impl fmt::Display for Currency {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::USD => write!(f, "USD"),
|
|
Self::EUR => write!(f, "EUR"),
|
|
Self::GBP => write!(f, "GBP"),
|
|
Self::JPY => write!(f, "JPY"),
|
|
Self::CHF => write!(f, "CHF"),
|
|
Self::CAD => write!(f, "CAD"),
|
|
Self::AUD => write!(f, "AUD"),
|
|
Self::NZD => write!(f, "NZD"),
|
|
Self::BTC => write!(f, "BTC"),
|
|
Self::ETH => write!(f, "ETH"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Currency {
|
|
/// Returns the default currency (USD)
|
|
fn default() -> Self {
|
|
Self::USD
|
|
}
|
|
}
|
|
|
|
/// Time in force enumeration - CANONICAL DEFINITION
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum TimeInForce {
|
|
/// Order is valid for the current trading day only
|
|
Day,
|
|
/// Order remains active until explicitly cancelled
|
|
GoodTillCancel,
|
|
/// Order must be executed immediately or cancelled
|
|
ImmediateOrCancel,
|
|
/// Order must be executed completely or cancelled
|
|
FillOrKill,
|
|
}
|
|
|
|
impl fmt::Display for TimeInForce {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Day => write!(f, "DAY"),
|
|
Self::GoodTillCancel => write!(f, "GTC"),
|
|
Self::ImmediateOrCancel => write!(f, "IOC"),
|
|
Self::FillOrKill => write!(f, "FOK"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for TimeInForce {
|
|
/// Returns the default time in force (Day)
|
|
fn default() -> Self {
|
|
Self::Day
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// CORE ID TYPES (MIGRATED FROM TRADING_ENGINE)
|
|
// =============================================================================
|
|
|
|
// Duplicate TradeId removed - using definition from line 1008
|
|
|
|
/// Event identifier for tracking system events
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub struct EventId(String);
|
|
|
|
impl EventId {
|
|
/// Create a new random event ID
|
|
pub fn new() -> Self {
|
|
use uuid::Uuid;
|
|
Self(Uuid::new_v4().to_string())
|
|
}
|
|
|
|
/// Create an event ID from a string, generating new if empty
|
|
pub fn from_string<S: Into<String>>(id: S) -> Self {
|
|
let id = id.into();
|
|
if id.is_empty() {
|
|
Self::new() // Generate new ID if empty
|
|
} else {
|
|
Self(id)
|
|
}
|
|
}
|
|
|
|
/// Get the string value of the event ID
|
|
pub fn value(&self) -> &str {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for EventId {
|
|
/// Format the event ID for display
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.0)
|
|
}
|
|
}
|
|
|
|
impl From<String> for EventId {
|
|
/// Create an `EventId` from a String
|
|
fn from(s: String) -> Self {
|
|
Self(s)
|
|
}
|
|
}
|
|
|
|
impl Default for EventId {
|
|
/// Create a default `EventId` with a new UUID
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Decimal Extension Trait
|
|
// =============================================================================
|
|
|
|
/// Extension trait for `rust_decimal::Decimal` with convenience methods
|
|
pub trait DecimalExt {
|
|
/// Create Decimal from f64, compatible with legacy `from_f64` usage
|
|
fn from_f64(value: f64) -> Option<Self>
|
|
where
|
|
Self: Sized;
|
|
|
|
/// Calculate square root of Decimal
|
|
fn sqrt(&self) -> Option<Self>
|
|
where
|
|
Self: Sized;
|
|
}
|
|
|
|
impl DecimalExt for Decimal {
|
|
fn from_f64(value: f64) -> Option<Self> {
|
|
Decimal::from_f64_retain(value)
|
|
}
|
|
|
|
fn sqrt(&self) -> Option<Self> {
|
|
if self.is_sign_negative() {
|
|
return None;
|
|
}
|
|
let value_f64: f64 = self.to_string().parse().ok()?;
|
|
let sqrt_f64 = value_f64.sqrt();
|
|
Decimal::from_f64_retain(sqrt_f64)
|
|
}
|
|
}
|
|
|
|
/// Fill identifier with validation
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub struct FillId(String);
|
|
|
|
impl FillId {
|
|
/// Create a new fill ID with validation
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
|
|
let id = id.into();
|
|
if id.is_empty() {
|
|
return Err(CommonTypeError::ValidationError {
|
|
field: "fill_id".to_owned(),
|
|
reason: "Fill ID cannot be empty".to_owned(),
|
|
});
|
|
}
|
|
Ok(Self(id))
|
|
}
|
|
|
|
/// Get the fill ID as a string slice
|
|
pub fn as_str(&self) -> &str {
|
|
&self.0
|
|
}
|
|
/// Convert the fill ID into an owned string
|
|
///
|
|
/// Convert the execution ID into an owned string
|
|
///
|
|
/// Convert execution ID into owned string
|
|
pub fn into_string(self) -> String {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for FillId {
|
|
/// Format the fill ID for display
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.0)
|
|
}
|
|
}
|
|
|
|
/// Aggregate identifier with validation
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub struct AggregateId(String);
|
|
|
|
impl AggregateId {
|
|
/// Create a new aggregate ID with validation
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
|
|
let id = id.into();
|
|
if id.is_empty() {
|
|
return Err(CommonTypeError::ValidationError {
|
|
field: "aggregate_id".to_owned(),
|
|
reason: "Aggregate ID cannot be empty".to_owned(),
|
|
});
|
|
}
|
|
Ok(Self(id))
|
|
}
|
|
|
|
/// Get the aggregate ID as a string slice
|
|
pub fn as_str(&self) -> &str {
|
|
&self.0
|
|
}
|
|
/// Convert the aggregate ID into an owned string
|
|
pub fn into_string(self) -> String {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for AggregateId {
|
|
/// Format the aggregate ID for display
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.0)
|
|
}
|
|
}
|
|
|
|
/// Asset identifier with validation
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub struct AssetId(String);
|
|
|
|
impl AssetId {
|
|
/// Create a new asset ID with validation
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
|
|
let id = id.into();
|
|
if id.is_empty() {
|
|
return Err(CommonTypeError::ValidationError {
|
|
field: "asset_id".to_owned(),
|
|
reason: "Asset ID cannot be empty".to_owned(),
|
|
});
|
|
}
|
|
Ok(Self(id))
|
|
}
|
|
|
|
/// Get the asset ID as a string slice
|
|
pub fn as_str(&self) -> &str {
|
|
&self.0
|
|
}
|
|
/// Convert the asset ID into an owned string
|
|
pub fn into_string(self) -> String {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for AssetId {
|
|
/// Format the asset ID for display
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.0)
|
|
}
|
|
}
|
|
|
|
/// Client identifier with validation
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub struct ClientId(String);
|
|
|
|
impl ClientId {
|
|
/// Create a new client ID with validation
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
|
|
let id = id.into();
|
|
if id.is_empty() {
|
|
return Err(CommonTypeError::ValidationError {
|
|
field: "client_id".to_owned(),
|
|
reason: "Client ID cannot be empty".to_owned(),
|
|
});
|
|
}
|
|
Ok(Self(id))
|
|
}
|
|
|
|
/// Get the client ID as a string slice
|
|
pub fn as_str(&self) -> &str {
|
|
&self.0
|
|
}
|
|
/// Convert the client ID into an owned string
|
|
pub fn into_string(self) -> String {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for ClientId {
|
|
/// Format the client ID for display
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.0)
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// CORE TRADING TYPES - MIGRATED FROM TRADING_ENGINE
|
|
// =============================================================================
|
|
|
|
/// Canonical Order struct - UNIFIED DEFINITION based on Agent 1's comprehensive analysis
|
|
///
|
|
/// This represents the single source of truth for Order across all services
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[cfg_attr(feature = "database", derive(sqlx::FromRow))]
|
|
pub struct Order {
|
|
// Core Identity
|
|
/// Unique order identifier
|
|
pub id: OrderId,
|
|
/// Client-provided order identifier
|
|
pub client_order_id: Option<String>,
|
|
/// Broker-assigned order identifier
|
|
pub broker_order_id: Option<String>,
|
|
/// Account identifier for the order
|
|
pub account_id: Option<String>,
|
|
|
|
// Trading Details
|
|
/// Trading symbol for the order
|
|
pub symbol: Symbol,
|
|
/// Order side (buy or sell)
|
|
pub side: OrderSide,
|
|
/// Type of order (market, limit, etc.)
|
|
pub order_type: OrderType,
|
|
/// Current status of the order
|
|
pub status: OrderStatus,
|
|
/// Time in force policy
|
|
pub time_in_force: TimeInForce,
|
|
|
|
// Quantities & Pricing
|
|
/// Total order quantity
|
|
pub quantity: Quantity,
|
|
/// Limit price for the order
|
|
pub price: Option<Price>,
|
|
/// Stop price for stop orders
|
|
pub stop_price: Option<Price>,
|
|
/// Quantity that has been filled
|
|
pub filled_quantity: Quantity,
|
|
/// Remaining quantity to be filled
|
|
pub remaining_quantity: Quantity,
|
|
/// Average execution price
|
|
pub average_price: Option<Price>,
|
|
/// Alias for `average_price` for database compatibility
|
|
pub avg_fill_price: Option<Price>,
|
|
/// Alias for `average_price` for API compatibility
|
|
pub average_fill_price: Option<Price>,
|
|
/// Exchange-assigned order identifier
|
|
pub exchange_order_id: Option<String>,
|
|
|
|
// Strategy Fields (from Agent 1)
|
|
/// Parent order ID for iceberg/algo orders
|
|
pub parent_id: Option<String>,
|
|
/// Execution algorithm name
|
|
pub execution_algorithm: Option<String>,
|
|
/// Execution algorithm parameters stored as JSON
|
|
pub execution_params: Value,
|
|
|
|
// Risk Management (from Agent 1)
|
|
/// Stop loss price for risk management
|
|
pub stop_loss: Option<Price>,
|
|
/// Take profit price for profit taking
|
|
pub take_profit: Option<Price>,
|
|
|
|
// Timestamps
|
|
/// Order creation timestamp
|
|
pub created_at: HftTimestamp,
|
|
/// Last update timestamp
|
|
pub updated_at: Option<HftTimestamp>,
|
|
/// Order expiration timestamp
|
|
pub expires_at: Option<HftTimestamp>,
|
|
|
|
// Extensibility
|
|
/// Additional order metadata stored as JSON
|
|
pub metadata: Value,
|
|
}
|
|
|
|
impl Order {
|
|
/// Create a new order with canonical fields
|
|
pub fn new(
|
|
symbol: Symbol,
|
|
side: OrderSide,
|
|
quantity: Quantity,
|
|
price: Option<Price>,
|
|
order_type: OrderType,
|
|
) -> Self {
|
|
let now = HftTimestamp::now_or_zero();
|
|
Self {
|
|
// Core Identity
|
|
id: OrderId::new(),
|
|
client_order_id: None,
|
|
broker_order_id: None,
|
|
account_id: None,
|
|
|
|
// Trading Details
|
|
symbol,
|
|
side,
|
|
order_type,
|
|
status: OrderStatus::Created,
|
|
time_in_force: TimeInForce::default(),
|
|
|
|
// Quantities & Pricing
|
|
quantity,
|
|
price,
|
|
stop_price: None,
|
|
filled_quantity: Quantity::ZERO,
|
|
remaining_quantity: quantity,
|
|
average_price: None,
|
|
avg_fill_price: None, // Database compatibility alias
|
|
average_fill_price: None, // API compatibility alias
|
|
exchange_order_id: None,
|
|
|
|
// Strategy Fields
|
|
parent_id: None,
|
|
execution_algorithm: None,
|
|
execution_params: serde_json::json!({}),
|
|
|
|
// Risk Management
|
|
stop_loss: None,
|
|
take_profit: None,
|
|
|
|
// Timestamps
|
|
created_at: now,
|
|
updated_at: None,
|
|
expires_at: None,
|
|
|
|
// Extensibility
|
|
metadata: serde_json::json!({}),
|
|
}
|
|
}
|
|
|
|
/// Check if the order is fully filled
|
|
pub fn is_filled(&self) -> bool {
|
|
self.filled_quantity == self.quantity
|
|
}
|
|
|
|
/// Check if the order is partially filled
|
|
pub fn is_partially_filled(&self) -> bool {
|
|
self.filled_quantity > Quantity::ZERO && self.filled_quantity < self.quantity
|
|
}
|
|
|
|
/// Calculate fill percentage
|
|
#[allow(clippy::float_arithmetic)]
|
|
pub fn fill_percentage(&self) -> f64 {
|
|
if self.quantity.is_zero() {
|
|
0.0
|
|
} else {
|
|
(self.filled_quantity.to_f64() / self.quantity.to_f64()) * 100.0
|
|
}
|
|
}
|
|
|
|
/// Set client order ID for tracking
|
|
pub fn with_client_order_id(mut self, client_order_id: String) -> Self {
|
|
self.client_order_id = Some(client_order_id);
|
|
self
|
|
}
|
|
|
|
/// Set account ID
|
|
pub fn with_account_id(mut self, account_id: String) -> Self {
|
|
self.account_id = Some(account_id);
|
|
self
|
|
}
|
|
|
|
/// Set time in force
|
|
pub const fn with_time_in_force(mut self, time_in_force: TimeInForce) -> Self {
|
|
self.time_in_force = time_in_force;
|
|
self
|
|
}
|
|
|
|
/// Set stop price
|
|
pub const fn with_stop_price(mut self, stop_price: Price) -> Self {
|
|
self.stop_price = Some(stop_price);
|
|
self
|
|
}
|
|
|
|
/// Set execution algorithm
|
|
pub fn with_execution_algorithm(mut self, algorithm: String) -> Self {
|
|
self.execution_algorithm = Some(algorithm);
|
|
self
|
|
}
|
|
|
|
/// Add execution parameter
|
|
pub fn with_execution_param(mut self, key: String, value: f64) -> Self {
|
|
if let Some(obj) = self.execution_params.as_object_mut() {
|
|
obj.insert(key, serde_json::to_value(value).unwrap_or(Value::Null));
|
|
} else {
|
|
let mut map = serde_json::Map::new();
|
|
map.insert(key, serde_json::to_value(value).unwrap_or(Value::Null));
|
|
self.execution_params = Value::Object(map);
|
|
}
|
|
self
|
|
}
|
|
|
|
/// Set stop loss
|
|
pub const fn with_stop_loss(mut self, stop_loss: Price) -> Self {
|
|
self.stop_loss = Some(stop_loss);
|
|
self
|
|
}
|
|
|
|
/// Set take profit
|
|
pub const fn with_take_profit(mut self, take_profit: Price) -> Self {
|
|
self.take_profit = Some(take_profit);
|
|
self
|
|
}
|
|
|
|
/// Add metadata
|
|
pub fn with_metadata(mut self, key: String, value: String) -> Self {
|
|
if let Some(obj) = self.metadata.as_object_mut() {
|
|
obj.insert(key, Value::String(value));
|
|
} else {
|
|
let mut map = serde_json::Map::new();
|
|
map.insert(key, Value::String(value));
|
|
self.metadata = Value::Object(map);
|
|
}
|
|
self
|
|
}
|
|
|
|
/// Update order status and timestamp
|
|
pub fn update_status(&mut self, status: OrderStatus) {
|
|
self.status = status;
|
|
self.updated_at = Some(HftTimestamp::now_or_zero());
|
|
}
|
|
|
|
/// Fill order with given quantity and price
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
#[allow(clippy::float_arithmetic)]
|
|
pub fn fill(
|
|
&mut self,
|
|
fill_quantity: Quantity,
|
|
fill_price: Price,
|
|
) -> Result<(), CommonTypeError> {
|
|
// Use Add trait instead of checked_add (Quantity doesn't have checked_add method)
|
|
let new_filled_value = self.filled_quantity.to_f64() + fill_quantity.to_f64();
|
|
if !new_filled_value.is_finite() || new_filled_value < 0.0 {
|
|
return Err(CommonTypeError::ValidationError {
|
|
field: "fill_quantity".to_owned(),
|
|
reason: "Fill quantity overflow".to_owned(),
|
|
});
|
|
}
|
|
let new_filled = Quantity::from_f64(new_filled_value).map_err(|e| CommonTypeError::ValidationError {
|
|
field: "fill_quantity".to_owned(),
|
|
reason: format!("Fill quantity overflow: {}", e),
|
|
})?;
|
|
if new_filled > self.quantity {
|
|
return Err(CommonTypeError::ValidationError {
|
|
field: "fill_quantity".to_owned(),
|
|
reason: "Fill quantity exceeds remaining quantity".to_owned(),
|
|
});
|
|
}
|
|
|
|
// Update filled quantity
|
|
let previous_filled = self.filled_quantity;
|
|
let new_filled_value = self.filled_quantity.value.checked_add(fill_quantity.value).ok_or_else(|| CommonTypeError::ValidationError {
|
|
field: "filled_quantity".to_owned(),
|
|
reason: "Filled quantity overflow".to_owned(),
|
|
})?;
|
|
self.filled_quantity = Quantity { value: new_filled_value };
|
|
|
|
let new_remaining_value = self.quantity.value.checked_sub(self.filled_quantity.value).ok_or_else(|| CommonTypeError::ValidationError {
|
|
field: "remaining_quantity".to_owned(),
|
|
reason: "Remaining quantity underflow".to_owned(),
|
|
})?;
|
|
self.remaining_quantity = Quantity { value: new_remaining_value };
|
|
|
|
// Update average price
|
|
if let Some(avg_price) = self.average_price {
|
|
let total_value = avg_price.to_f64() * previous_filled.to_f64()
|
|
+ fill_price.to_f64() * fill_quantity.to_f64();
|
|
let new_avg = Some(
|
|
Price::from_f64(total_value / self.filled_quantity.to_f64()).unwrap_or(fill_price),
|
|
);
|
|
self.average_price = new_avg;
|
|
self.avg_fill_price = new_avg; // Keep in sync
|
|
} else {
|
|
self.average_price = Some(fill_price);
|
|
self.avg_fill_price = Some(fill_price); // Keep in sync
|
|
}
|
|
|
|
// Update status
|
|
if self.is_filled() {
|
|
self.update_status(OrderStatus::Filled);
|
|
} else {
|
|
self.update_status(OrderStatus::PartiallyFilled);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Create a limit order - convenience constructor
|
|
pub fn limit(symbol: Symbol, side: OrderSide, quantity: Quantity, price: Price) -> Self {
|
|
Self::new(symbol, side, quantity, Some(price), OrderType::Limit)
|
|
}
|
|
|
|
/// Create a market order - convenience constructor
|
|
pub fn market(symbol: Symbol, side: OrderSide, quantity: Quantity) -> Self {
|
|
Self::new(symbol, side, quantity, None, OrderType::Market)
|
|
}
|
|
|
|
/// Get symbol hash for performance-critical operations
|
|
pub fn symbol_hash(&self) -> i64 {
|
|
use std::collections::hash_map::DefaultHasher;
|
|
use std::hash::{Hash, Hasher};
|
|
|
|
let mut hasher = DefaultHasher::new();
|
|
self.symbol.as_str().hash(&mut hasher);
|
|
i64::try_from(hasher.finish()).unwrap_or(0)
|
|
}
|
|
|
|
/// Get order timestamp
|
|
pub const fn timestamp(&self) -> HftTimestamp {
|
|
self.created_at
|
|
}
|
|
}
|
|
|
|
impl Default for Order {
|
|
fn default() -> Self {
|
|
Self {
|
|
id: OrderId::new(),
|
|
client_order_id: None,
|
|
broker_order_id: None,
|
|
account_id: None,
|
|
|
|
symbol: Symbol::from("DEFAULT"),
|
|
side: OrderSide::Buy,
|
|
order_type: OrderType::Market,
|
|
status: OrderStatus::Created,
|
|
time_in_force: TimeInForce::Day,
|
|
|
|
quantity: Quantity::ONE,
|
|
price: None,
|
|
stop_price: None,
|
|
filled_quantity: Quantity::ZERO,
|
|
remaining_quantity: Quantity::ONE,
|
|
average_price: None,
|
|
avg_fill_price: None,
|
|
average_fill_price: None,
|
|
exchange_order_id: None,
|
|
|
|
parent_id: None,
|
|
execution_algorithm: None,
|
|
execution_params: serde_json::json!({}),
|
|
|
|
stop_loss: None,
|
|
take_profit: None,
|
|
|
|
created_at: HftTimestamp::now().unwrap_or(HftTimestamp { nanos: 0 }),
|
|
updated_at: None,
|
|
expires_at: None,
|
|
|
|
metadata: serde_json::json!({}),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents a trading position - CANONICAL DEFINITION
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[cfg_attr(feature = "database", derive(sqlx::FromRow))]
|
|
pub struct Position {
|
|
/// Unique position identifier
|
|
pub id: Uuid,
|
|
|
|
/// Trading symbol
|
|
pub symbol: String,
|
|
|
|
/// Position quantity (positive for long, negative for short)
|
|
pub quantity: Decimal,
|
|
|
|
/// Average entry price
|
|
pub avg_price: Decimal,
|
|
|
|
/// Average cost per share
|
|
pub avg_cost: Decimal,
|
|
|
|
/// Cost basis for tax calculations
|
|
pub basis: Decimal,
|
|
|
|
/// Average entry price
|
|
pub average_price: Decimal,
|
|
|
|
/// Market value of position
|
|
pub market_value: Decimal,
|
|
|
|
/// Unrealized P&L
|
|
pub unrealized_pnl: Decimal,
|
|
|
|
/// Realized P&L
|
|
pub realized_pnl: Decimal,
|
|
|
|
/// Position creation timestamp
|
|
pub created_at: DateTime<Utc>,
|
|
|
|
/// Last update timestamp
|
|
pub updated_at: DateTime<Utc>,
|
|
|
|
/// Last updated timestamp
|
|
pub last_updated: DateTime<Utc>,
|
|
|
|
/// Current market price (for P&L calculation)
|
|
pub current_price: Option<Decimal>,
|
|
|
|
/// Position size in base currency
|
|
pub notional_value: Decimal,
|
|
|
|
/// Margin requirement
|
|
pub margin_requirement: Decimal,
|
|
}
|
|
|
|
impl Position {
|
|
/// Create a new position
|
|
pub fn new(symbol: String, quantity: Decimal, avg_price: Decimal) -> Self {
|
|
let now = Utc::now();
|
|
let notional_value = quantity.abs().checked_mul(avg_price).unwrap_or(Decimal::ZERO);
|
|
|
|
Self {
|
|
id: Uuid::new_v4(),
|
|
symbol,
|
|
quantity,
|
|
avg_price,
|
|
avg_cost: avg_price, // Keep avg_cost synchronized with avg_price
|
|
basis: quantity.checked_mul(avg_price).unwrap_or(Decimal::ZERO), // Cost basis calculation
|
|
average_price: avg_price, // Same as avg_price for compatibility
|
|
market_value: notional_value, // Initialize market value to notional value
|
|
unrealized_pnl: Decimal::ZERO,
|
|
realized_pnl: Decimal::ZERO,
|
|
created_at: now,
|
|
updated_at: now,
|
|
last_updated: now, // Same as updated_at for compatibility
|
|
current_price: None,
|
|
notional_value,
|
|
margin_requirement: notional_value.checked_mul(
|
|
Decimal::from_str_exact("0.02").unwrap_or(Decimal::ZERO)
|
|
).unwrap_or(Decimal::ZERO), // 2% margin
|
|
}
|
|
}
|
|
|
|
/// Check if position is long
|
|
pub fn is_long(&self) -> bool {
|
|
self.quantity > Decimal::ZERO
|
|
}
|
|
|
|
/// Check if position is short
|
|
pub fn is_short(&self) -> bool {
|
|
self.quantity < Decimal::ZERO
|
|
}
|
|
|
|
/// Calculate unrealized P&L based on current price
|
|
pub fn calculate_unrealized_pnl(&mut self, current_price: Decimal) {
|
|
self.current_price = Some(current_price);
|
|
self.market_value = self.quantity.abs().checked_mul(current_price).unwrap_or(Decimal::ZERO);
|
|
// For both long and short: quantity * (current_price - avg_price)
|
|
let price_diff = current_price.checked_sub(self.avg_price).unwrap_or(Decimal::ZERO);
|
|
self.unrealized_pnl = self.quantity.checked_mul(price_diff).unwrap_or(Decimal::ZERO);
|
|
let now = Utc::now();
|
|
self.updated_at = now;
|
|
self.last_updated = now; // Keep alias synchronized
|
|
}
|
|
|
|
/// Get total P&L (realized + unrealized)
|
|
pub fn total_pnl(&self) -> Decimal {
|
|
self.realized_pnl.checked_add(self.unrealized_pnl).unwrap_or(Decimal::ZERO)
|
|
}
|
|
|
|
/// Calculate return on investment percentage
|
|
pub fn roi_percentage(&self) -> Decimal {
|
|
if self.notional_value.is_zero() {
|
|
Decimal::ZERO
|
|
} else {
|
|
let pnl_ratio = self.total_pnl().checked_div(self.notional_value).unwrap_or(Decimal::ZERO);
|
|
pnl_ratio.checked_mul(Decimal::from(100)).unwrap_or(Decimal::ZERO)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents a trade execution - CANONICAL DEFINITION
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[cfg_attr(feature = "database", derive(sqlx::FromRow))]
|
|
pub struct Execution {
|
|
/// Unique execution identifier
|
|
pub id: Uuid,
|
|
|
|
/// Related order ID
|
|
pub order_id: Uuid,
|
|
|
|
/// Trading symbol
|
|
pub symbol: String,
|
|
|
|
/// Executed quantity
|
|
pub quantity: Decimal,
|
|
|
|
/// Execution price
|
|
pub price: Decimal,
|
|
|
|
/// Execution side
|
|
pub side: OrderSide,
|
|
|
|
/// Trading fees
|
|
pub fees: Decimal,
|
|
|
|
/// Fee currency
|
|
pub fee_currency: String,
|
|
|
|
/// Execution timestamp
|
|
pub executed_at: DateTime<Utc>,
|
|
|
|
/// Execution timestamp
|
|
pub timestamp: DateTime<Utc>,
|
|
|
|
/// Symbol hash for performance
|
|
pub symbol_hash: i64,
|
|
|
|
/// Broker execution ID
|
|
pub broker_execution_id: Option<String>,
|
|
|
|
/// Counterparty information
|
|
pub counterparty: Option<String>,
|
|
|
|
/// Trade venue
|
|
pub venue: Option<String>,
|
|
|
|
/// Gross trade value
|
|
pub gross_value: Decimal,
|
|
|
|
/// Net trade value (after fees)
|
|
pub net_value: Decimal,
|
|
}
|
|
|
|
impl Execution {
|
|
/// Create a new execution
|
|
pub fn new(
|
|
order_id: Uuid,
|
|
symbol: String,
|
|
quantity: Decimal,
|
|
price: Decimal,
|
|
side: OrderSide,
|
|
fees: Decimal,
|
|
) -> Self {
|
|
let gross_value = quantity.checked_mul(price).unwrap_or(Decimal::ZERO);
|
|
let net_value = if side == OrderSide::Buy {
|
|
gross_value.checked_add(fees).unwrap_or(gross_value)
|
|
} else {
|
|
gross_value.checked_sub(fees).unwrap_or(gross_value)
|
|
};
|
|
let now = Utc::now();
|
|
let symbol_hash = Self::hash_symbol(&symbol);
|
|
|
|
Self {
|
|
id: Uuid::new_v4(),
|
|
order_id,
|
|
symbol,
|
|
quantity,
|
|
price,
|
|
side,
|
|
fees,
|
|
fee_currency: "USD".to_owned(), // Default to USD
|
|
executed_at: now,
|
|
timestamp: now, // Same as executed_at for compatibility
|
|
symbol_hash,
|
|
broker_execution_id: None,
|
|
counterparty: None,
|
|
venue: None,
|
|
gross_value,
|
|
net_value,
|
|
}
|
|
}
|
|
|
|
/// Calculate effective price including fees
|
|
pub fn effective_price(&self) -> Decimal {
|
|
if self.quantity.is_zero() {
|
|
self.price
|
|
} else {
|
|
self.net_value.checked_div(self.quantity).unwrap_or(self.price)
|
|
}
|
|
}
|
|
|
|
/// Hash symbol for performance
|
|
fn hash_symbol(symbol: &str) -> i64 {
|
|
use std::collections::hash_map::DefaultHasher;
|
|
use std::hash::{Hash, Hasher};
|
|
|
|
let mut hasher = DefaultHasher::new();
|
|
symbol.hash(&mut hasher);
|
|
i64::try_from(hasher.finish()).unwrap_or(0)
|
|
}
|
|
}
|
|
|
|
/// Core Price type using fixed-point arithmetic for precision
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
|
pub struct Price {
|
|
value: u64,
|
|
}
|
|
|
|
impl Price {
|
|
/// Zero price constant
|
|
pub const ZERO: Self = Self { value: 0 };
|
|
/// One unit price constant (1.0)
|
|
pub const ONE: Self = Self { value: 100_000_000 };
|
|
/// One cent constant (0.01)
|
|
pub const CENT: Self = Self { value: 1_000_000 };
|
|
/// Maximum price value
|
|
pub const MAX: Self = Self { value: u64::MAX };
|
|
|
|
/// Create a Price from a floating-point value
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
#[allow(clippy::as_conversions)]
|
|
#[allow(clippy::float_arithmetic)]
|
|
pub fn from_f64(value: f64) -> Result<Self, CommonTypeError> {
|
|
if value < 0.0_f64 || !value.is_finite() {
|
|
return Err(CommonTypeError::InvalidPrice {
|
|
value: value.to_string(),
|
|
reason: "Price validation failed".to_owned(),
|
|
});
|
|
}
|
|
Ok(Self {
|
|
#[allow(clippy::as_conversions)]
|
|
#[allow(clippy::float_arithmetic)]
|
|
value: (value * 100_000_000.0).round() as u64,
|
|
})
|
|
}
|
|
|
|
/// Convert to floating-point representation
|
|
#[must_use]
|
|
#[allow(clippy::float_arithmetic)]
|
|
/// Convert the quantity to a floating point value
|
|
#[allow(clippy::as_conversions)]
|
|
pub fn to_f64(&self) -> f64 {
|
|
#[allow(clippy::as_conversions)]
|
|
{ self.value as f64 / 100_000_000.0 }
|
|
}
|
|
|
|
/// Get floating-point representation (alias for `to_f64`)
|
|
///
|
|
/// Convert quantity to f64 representation
|
|
///
|
|
/// Convert quantity to f64 representation
|
|
///
|
|
/// Convert quantity to f64 representation
|
|
#[must_use]
|
|
pub fn as_f64(&self) -> f64 {
|
|
self.to_f64()
|
|
}
|
|
|
|
/// Create a zero price
|
|
///
|
|
/// Create a zero quantity
|
|
///
|
|
/// Create zero quantity
|
|
#[must_use]
|
|
pub const fn zero() -> Self {
|
|
Self::ZERO
|
|
}
|
|
|
|
/// Convert to Decimal type for precise calculations
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn to_decimal(&self) -> Result<Decimal, CommonTypeError> {
|
|
<Decimal as DecimalExt>::from_f64(self.to_f64()).ok_or_else(|| CommonTypeError::InvalidPrice {
|
|
value: "0.0".to_owned(),
|
|
reason: "Price to Decimal conversion failed".to_owned(),
|
|
})
|
|
}
|
|
|
|
/// Create a Price from a Decimal value
|
|
#[must_use]
|
|
pub fn from_decimal(decimal: Decimal) -> Self {
|
|
Self::from(decimal)
|
|
}
|
|
|
|
/// Create a new Price (alias for `from_f64`)
|
|
///
|
|
/// Create a new quantity from a floating point value
|
|
///
|
|
/// Create new quantity from f64 value
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn new(value: f64) -> Result<Self, CommonTypeError> {
|
|
Self::from_f64(value)
|
|
}
|
|
|
|
/// Get the raw internal value representation
|
|
///
|
|
/// Get the raw internal value
|
|
///
|
|
/// Get the raw internal value representation
|
|
#[must_use]
|
|
pub const fn raw_value(&self) -> u64 {
|
|
self.value
|
|
}
|
|
|
|
/// Get the price as a u64 value (same as `raw_value`)
|
|
///
|
|
/// Convert to u64 representation
|
|
///
|
|
/// Convert quantity to u64 representation
|
|
#[must_use]
|
|
pub const fn as_u64(&self) -> u64 {
|
|
self.value
|
|
}
|
|
|
|
/// Create a Price from a raw u64 value
|
|
///
|
|
/// Create a quantity from raw internal value
|
|
///
|
|
/// Create quantity from raw u64 value
|
|
#[must_use]
|
|
pub const fn from_raw(value: u64) -> Self {
|
|
Self { value }
|
|
}
|
|
|
|
/// Convert price to cents (divides by 1M for 8 decimal places)
|
|
#[must_use]
|
|
#[allow(clippy::integer_division)]
|
|
pub const fn to_cents(&self) -> u64 {
|
|
self.value / 1_000_000
|
|
}
|
|
|
|
/// Create a Price from cents value
|
|
#[must_use]
|
|
pub const fn from_cents(cents: u64) -> Self {
|
|
Self {
|
|
value: cents.saturating_mul(1_000_000),
|
|
}
|
|
}
|
|
|
|
/// Check if the price is zero
|
|
///
|
|
/// Check if the quantity is zero
|
|
///
|
|
/// Check if quantity is zero
|
|
#[must_use]
|
|
pub const fn is_zero(&self) -> bool {
|
|
self.value == 0
|
|
}
|
|
|
|
/// Check if the price is non-zero (has some value)
|
|
///
|
|
/// Check if the quantity is non-zero (has some value)
|
|
///
|
|
/// Check if quantity has a non-zero value
|
|
#[must_use]
|
|
pub const fn is_some(&self) -> bool {
|
|
!self.is_zero()
|
|
}
|
|
|
|
/// Check if the price is zero (has no value)
|
|
///
|
|
/// Check if the quantity is zero (has no value)
|
|
///
|
|
/// Check if quantity is zero (none)
|
|
#[must_use]
|
|
pub const fn is_none(&self) -> bool {
|
|
self.is_zero()
|
|
}
|
|
|
|
/// Get a reference to this price
|
|
///
|
|
/// Get a reference to self
|
|
///
|
|
/// Get a reference to self
|
|
#[must_use]
|
|
pub const fn as_ref(&self) -> &Self {
|
|
self
|
|
}
|
|
|
|
/// Get the absolute value of the price (prices are always positive)
|
|
///
|
|
/// Get the absolute value (quantities are always positive)
|
|
///
|
|
/// Get absolute value (always positive for Quantity)
|
|
#[must_use]
|
|
pub const fn abs(&self) -> Self {
|
|
*self
|
|
}
|
|
|
|
/// Multiply this price by another price
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
#[allow(clippy::arithmetic_side_effects)]
|
|
pub fn multiply(&self, other: Self) -> Result<Self, CommonTypeError> {
|
|
#[allow(clippy::arithmetic_side_effects)]
|
|
let result = *self * other;
|
|
result
|
|
}
|
|
|
|
/// Subtract another price from this price
|
|
///
|
|
/// Subtract another quantity from this quantity
|
|
///
|
|
/// Subtract another quantity from this quantity
|
|
///
|
|
/// Subtract another quantity from this quantity
|
|
///
|
|
/// Subtract another quantity from this quantity
|
|
#[must_use]
|
|
#[allow(clippy::arithmetic_side_effects)]
|
|
pub fn subtract(&self, other: Self) -> Self {
|
|
*self - other
|
|
}
|
|
|
|
/// Divide this price by a floating point divisor
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn divide(&self, divisor: f64) -> Result<Self, CommonTypeError> {
|
|
if divisor == 0.0 {
|
|
return Err(CommonTypeError::InvalidPrice {
|
|
value: format!("{:?}", self),
|
|
reason: "Division by zero".to_owned(),
|
|
});
|
|
}
|
|
#[allow(clippy::cast_precision_loss)]
|
|
#[allow(clippy::arithmetic_side_effects)]
|
|
let result = (self.value as f64 / divisor) as u64;
|
|
Ok(Self { value: result })
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Price {
|
|
/// Format the price for display with 8 decimal places
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{:.8}", self.to_f64())
|
|
}
|
|
}
|
|
|
|
impl Default for Price {
|
|
/// Returns the default price (zero)
|
|
fn default() -> Self {
|
|
Self::ZERO
|
|
}
|
|
}
|
|
|
|
impl FromStr for Price {
|
|
type Err = CommonTypeError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
let parsed_value = s
|
|
.parse::<f64>()
|
|
.map_err(|e| CommonTypeError::InvalidPrice {
|
|
value: s.to_owned(),
|
|
reason: format!("Cannot parse '{}' as price: {}", s, e),
|
|
})?;
|
|
Self::from_f64(parsed_value)
|
|
}
|
|
}
|
|
|
|
impl Add for Price {
|
|
type Output = Self;
|
|
fn add(self, rhs: Self) -> Self::Output {
|
|
Self {
|
|
value: self.value.saturating_add(rhs.value),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Sub for Price {
|
|
type Output = Self;
|
|
fn sub(self, rhs: Self) -> Self::Output {
|
|
Self {
|
|
value: self.value.saturating_sub(rhs.value),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Mul<f64> for Price {
|
|
#[allow(clippy::float_arithmetic)]
|
|
type Output = Result<Self, CommonTypeError>;
|
|
fn mul(self, rhs: f64) -> Self::Output {
|
|
Self::from_f64(self.to_f64() * rhs)
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::float_arithmetic)]
|
|
impl Div<f64> for Price {
|
|
type Output = Result<Self, CommonTypeError>;
|
|
fn div(self, rhs: f64) -> Self::Output {
|
|
if rhs == 0.0_f64 {
|
|
return Err(CommonTypeError::ConversionError {
|
|
message: "Cannot divide price by zero".to_owned(),
|
|
});
|
|
}
|
|
Self::from_f64(self.to_f64() / rhs)
|
|
}
|
|
}
|
|
|
|
impl From<Decimal> for Price {
|
|
fn from(decimal: Decimal) -> Self {
|
|
let f64_val: f64 = TryInto::<f64>::try_into(decimal).unwrap_or_else(|_| {
|
|
tracing::warn!("Failed to convert Decimal to f64, using 0.0 as fallback");
|
|
0.0_f64
|
|
});
|
|
Self::from_f64(f64_val).unwrap_or_else(|_| {
|
|
tracing::warn!(
|
|
"Failed to create Price from f64 value {}, using ZERO",
|
|
f64_val
|
|
);
|
|
Self::ZERO
|
|
})
|
|
}
|
|
}
|
|
|
|
impl From<Price> for Decimal {
|
|
fn from(price: Price) -> Self {
|
|
price.to_decimal().unwrap_or(Decimal::ZERO)
|
|
}
|
|
}
|
|
|
|
// TryFrom<Quantity> for Decimal removed due to conflicting blanket implementation
|
|
// Use qty.to_decimal() directly instead
|
|
impl From<Quantity> for Decimal {
|
|
fn from(qty: Quantity) -> Self {
|
|
qty.to_decimal().unwrap_or(Decimal::ZERO)
|
|
}
|
|
}
|
|
|
|
// TryFrom<Quantity> for Decimal removed due to conflict with From implementation
|
|
// Use the From implementation instead which handles errors by returning ZERO
|
|
|
|
// =============================================================================
|
|
// Decimal Extension Trait
|
|
// =============================================================================
|
|
|
|
impl Mul<Self> for Price {
|
|
#[allow(clippy::float_arithmetic)]
|
|
#[allow(clippy::arithmetic_side_effects)]
|
|
type Output = Result<Self, CommonTypeError>;
|
|
fn mul(self, rhs: Self) -> Self::Output {
|
|
Self::from_f64(self.to_f64() * rhs.to_f64())
|
|
}
|
|
}
|
|
|
|
impl TryFrom<String> for Price {
|
|
type Error = CommonTypeError;
|
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
|
Self::from_str(&s)
|
|
}
|
|
}
|
|
|
|
impl TryFrom<&str> for Price {
|
|
type Error = CommonTypeError;
|
|
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
|
Self::from_str(s)
|
|
}
|
|
}
|
|
|
|
impl PartialEq<f64> for Price {
|
|
#[allow(clippy::float_arithmetic)]
|
|
fn eq(&self, other: &f64) -> bool {
|
|
(self.to_f64() - other).abs() < f64::EPSILON
|
|
}
|
|
}
|
|
|
|
impl PartialEq<Price> for f64 {
|
|
#[allow(clippy::float_arithmetic)]
|
|
fn eq(&self, other: &Price) -> bool {
|
|
(self - other.to_f64()).abs() < f64::EPSILON
|
|
}
|
|
}
|
|
|
|
impl AddAssign for Price {
|
|
fn add_assign(&mut self, rhs: Self) {
|
|
self.value = self.value.saturating_add(rhs.value);
|
|
}
|
|
}
|
|
|
|
impl SubAssign for Price {
|
|
fn sub_assign(&mut self, rhs: Self) {
|
|
self.value = self.value.saturating_sub(rhs.value);
|
|
}
|
|
}
|
|
|
|
impl MulAssign<f64> for Price {
|
|
fn mul_assign(&mut self, rhs: f64) {
|
|
if let Ok(result) = self.mul(rhs) {
|
|
*self = result;
|
|
}
|
|
// If multiplication fails, self remains unchanged
|
|
}
|
|
}
|
|
|
|
impl DivAssign<f64> for Price {
|
|
fn div_assign(&mut self, rhs: f64) {
|
|
if let Ok(result) = self.div(rhs) {
|
|
*self = result;
|
|
}
|
|
// If division fails, self remains unchanged
|
|
}
|
|
}
|
|
|
|
impl PartialOrd<f64> for Price {
|
|
fn partial_cmp(&self, other: &f64) -> Option<std::cmp::Ordering> {
|
|
self.to_f64().partial_cmp(other)
|
|
}
|
|
}
|
|
|
|
impl PartialOrd<Price> for f64 {
|
|
fn partial_cmp(&self, other: &Price) -> Option<std::cmp::Ordering> {
|
|
self.partial_cmp(&other.to_f64())
|
|
}
|
|
}
|
|
|
|
/// Core Quantity type using fixed-point arithmetic
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
|
pub struct Quantity {
|
|
value: u64,
|
|
}
|
|
|
|
impl Quantity {
|
|
/// Zero quantity constant
|
|
pub const ZERO: Self = Self { value: 0 };
|
|
/// One unit quantity constant
|
|
pub const ONE: Self = Self { value: 100_000_000 };
|
|
/// Maximum possible quantity
|
|
pub const MAX: Self = Self { value: u64::MAX };
|
|
|
|
/// Create a Quantity from a floating point value
|
|
#[allow(clippy::as_conversions)]
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
#[allow(clippy::float_arithmetic)]
|
|
pub fn from_f64(value: f64) -> Result<Self, CommonTypeError> {
|
|
if value < 0.0_f64 || !value.is_finite() {
|
|
return Err(CommonTypeError::InvalidQuantity {
|
|
value: value.to_string(),
|
|
reason: "Quantity validation failed".to_owned(),
|
|
});
|
|
}
|
|
Ok(Self {
|
|
value: (value * 100_000_000.0).round() as u64,
|
|
})
|
|
}
|
|
|
|
/// Convert quantity to floating point representation
|
|
#[must_use]
|
|
#[allow(clippy::as_conversions)]
|
|
#[allow(clippy::float_arithmetic)]
|
|
pub fn to_f64(&self) -> f64 {
|
|
self.value as f64 / 100_000_000.0
|
|
}
|
|
|
|
/// Convert the quantity to a Decimal value
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn to_decimal(&self) -> Result<Decimal, CommonTypeError> {
|
|
Decimal::from_f64_retain(self.to_f64()).ok_or_else(|| CommonTypeError::InvalidQuantity {
|
|
value: "0.0".to_owned(),
|
|
reason: "Quantity to Decimal conversion failed".to_owned(),
|
|
})
|
|
}
|
|
|
|
/// Get the internal value representation
|
|
#[must_use]
|
|
pub const fn value(&self) -> u64 {
|
|
self.value
|
|
}
|
|
|
|
/// Get the raw internal value representation
|
|
#[must_use]
|
|
pub const fn raw_value(&self) -> u64 {
|
|
self.value
|
|
}
|
|
|
|
/// Convert quantity to u64 representation
|
|
#[must_use]
|
|
pub const fn as_u64(&self) -> u64 {
|
|
self.value
|
|
}
|
|
|
|
/// Create quantity from raw u64 value
|
|
#[must_use]
|
|
pub const fn from_raw(value: u64) -> Self {
|
|
Self { value }
|
|
}
|
|
|
|
/// Create new quantity from f64 value
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
#[allow(clippy::as_conversions)]
|
|
pub fn new(value: f64) -> Result<Self, CommonTypeError> {
|
|
Self::from_f64(value)
|
|
}
|
|
|
|
/// Create zero quantity
|
|
#[must_use]
|
|
pub const fn zero() -> Self {
|
|
#[allow(clippy::as_conversions)]
|
|
Self::ZERO
|
|
}
|
|
|
|
/// Create a quantity from an i64 value
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
#[allow(clippy::as_conversions)]
|
|
pub fn from_i64(value: i64) -> Result<Self, CommonTypeError> {
|
|
Self::from_f64(value as f64)
|
|
}
|
|
|
|
/// Create a quantity from a u64 value
|
|
///
|
|
/// # Errors
|
|
#[allow(clippy::as_conversions)]
|
|
/// Returns error if the operation fails
|
|
pub fn from_u64(value: u64) -> Result<Self, CommonTypeError> {
|
|
Self::from_f64(value as f64)
|
|
}
|
|
|
|
/// Create a quantity from a Decimal value
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn from_decimal(decimal: Decimal) -> Result<Self, CommonTypeError> {
|
|
use std::convert::TryFrom;
|
|
Self::try_from(decimal).map_err(|e| CommonTypeError::InvalidQuantity {
|
|
value: decimal.to_string(),
|
|
reason: format!("Failed to convert Decimal to Quantity: {}", e),
|
|
})
|
|
}
|
|
|
|
/// Check if quantity is zero
|
|
#[must_use]
|
|
pub const fn is_zero(&self) -> bool {
|
|
self.value == 0
|
|
}
|
|
|
|
/// Check if quantity has a non-zero value
|
|
#[must_use]
|
|
pub const fn is_some(&self) -> bool {
|
|
!self.is_zero()
|
|
}
|
|
|
|
/// Check if quantity is zero (none)
|
|
#[must_use]
|
|
pub const fn is_none(&self) -> bool {
|
|
self.is_zero()
|
|
}
|
|
|
|
/// Get a reference to self
|
|
#[must_use]
|
|
pub const fn as_ref(&self) -> &Self {
|
|
self
|
|
}
|
|
|
|
/// Get absolute value (always positive for Quantity)
|
|
#[must_use]
|
|
pub const fn abs(&self) -> Self {
|
|
*self
|
|
}
|
|
|
|
/// Get the sign of the quantity (1.0 for positive, 0.0 for zero)
|
|
#[must_use]
|
|
pub const fn signum(&self) -> f64 {
|
|
if self.value > 0 {
|
|
1.0
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
|
|
/// Check if quantity is positive
|
|
#[must_use]
|
|
pub const fn is_positive(&self) -> bool {
|
|
self.value > 0
|
|
}
|
|
|
|
/// Check if quantity is negative (always false for Quantity)
|
|
#[must_use]
|
|
pub const fn is_negative(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
/// Convert quantity to f64 representation
|
|
#[must_use]
|
|
pub fn as_f64(&self) -> f64 {
|
|
self.to_f64()
|
|
}
|
|
|
|
/// Create quantity from number of shares
|
|
#[must_use]
|
|
pub const fn from_shares(shares: u64) -> Self {
|
|
Self {
|
|
value: shares.saturating_mul(100_000_000),
|
|
}
|
|
}
|
|
|
|
/// Convert quantity to number of shares
|
|
#[must_use]
|
|
#[allow(clippy::integer_division)]
|
|
pub const fn to_shares(&self) -> u64 {
|
|
self.value / 100_000_000
|
|
}
|
|
|
|
/// Multiply this quantity by another quantity
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
#[allow(clippy::float_arithmetic)]
|
|
#[allow(clippy::arithmetic_side_effects)]
|
|
pub fn multiply(&self, other: Self) -> Result<Self, CommonTypeError> {
|
|
Self::from_f64(self.to_f64() * other.to_f64())
|
|
}
|
|
|
|
/// Subtract another quantity from this quantity
|
|
#[must_use]
|
|
pub fn subtract(&self, other: Self) -> Self {
|
|
Self {
|
|
value: self.value.checked_sub(other.value).unwrap_or_else(|| {
|
|
tracing::warn!(
|
|
"Quantity subtraction underflow: {} - {}, returning 0",
|
|
self.value, other.value
|
|
);
|
|
0
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Checked addition with overflow protection
|
|
#[must_use]
|
|
pub const fn checked_add(&self, other: Self) -> Option<Self> {
|
|
match self.value.checked_add(other.value) {
|
|
Some(value) => Some(Self { value }),
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
/// Checked subtraction with underflow protection
|
|
#[must_use]
|
|
pub const fn checked_sub(&self, other: Self) -> Option<Self> {
|
|
match self.value.checked_sub(other.value) {
|
|
Some(value) => Some(Self { value }),
|
|
None => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Quantity {
|
|
fn default() -> Self {
|
|
Self::ZERO
|
|
}
|
|
}
|
|
|
|
impl FromStr for Quantity {
|
|
type Err = CommonTypeError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
let parsed_value = s
|
|
.parse::<f64>()
|
|
.map_err(|e| CommonTypeError::InvalidQuantity {
|
|
value: s.to_owned(),
|
|
reason: format!("Cannot parse '{}' as quantity: {}", s, e),
|
|
})?;
|
|
Self::from_f64(parsed_value)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Quantity {
|
|
/// Format the quantity for display with 8 decimal places
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{:.8}", self.to_f64())
|
|
}
|
|
}
|
|
|
|
impl TryFrom<i32> for Quantity {
|
|
type Error = CommonTypeError;
|
|
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
|
Self::new(f64::from(value))
|
|
}
|
|
}
|
|
|
|
impl TryFrom<u64> for Quantity {
|
|
type Error = CommonTypeError;
|
|
fn try_from(value: u64) -> Result<Self, Self::Error> {
|
|
#[allow(clippy::as_conversions)]
|
|
let f64_value = value as f64;
|
|
Self::new(f64_value)
|
|
}
|
|
}
|
|
|
|
impl TryFrom<f64> for Quantity {
|
|
type Error = CommonTypeError;
|
|
fn try_from(value: f64) -> Result<Self, Self::Error> {
|
|
Self::new(value)
|
|
}
|
|
}
|
|
|
|
impl TryFrom<Decimal> for Quantity {
|
|
type Error = CommonTypeError;
|
|
fn try_from(decimal: Decimal) -> Result<Self, Self::Error> {
|
|
let f64_val: f64 =
|
|
TryInto::<f64>::try_into(decimal).map_err(|e| CommonTypeError::ConversionError {
|
|
message: format!("Failed to convert Decimal to f64: {}", e),
|
|
})?;
|
|
Self::from_f64(f64_val)
|
|
}
|
|
}
|
|
|
|
impl TryFrom<String> for Quantity {
|
|
type Error = CommonTypeError;
|
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
|
Self::from_str(&s)
|
|
}
|
|
}
|
|
|
|
impl TryFrom<&str> for Quantity {
|
|
type Error = CommonTypeError;
|
|
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
|
Self::from_str(s)
|
|
}
|
|
}
|
|
|
|
impl PartialEq<f64> for Quantity {
|
|
#[allow(clippy::float_arithmetic)]
|
|
fn eq(&self, other: &f64) -> bool {
|
|
(self.to_f64() - other).abs() < f64::EPSILON
|
|
}
|
|
}
|
|
|
|
impl PartialEq<Quantity> for f64 {
|
|
#[allow(clippy::float_arithmetic)]
|
|
fn eq(&self, other: &Quantity) -> bool {
|
|
(self - other.to_f64()).abs() < f64::EPSILON
|
|
}
|
|
}
|
|
|
|
impl PartialOrd<f64> for Quantity {
|
|
fn partial_cmp(&self, other: &f64) -> Option<std::cmp::Ordering> {
|
|
self.to_f64().partial_cmp(other)
|
|
}
|
|
}
|
|
|
|
impl PartialOrd<Quantity> for f64 {
|
|
fn partial_cmp(&self, other: &Quantity) -> Option<std::cmp::Ordering> {
|
|
self.partial_cmp(&other.to_f64())
|
|
}
|
|
}
|
|
|
|
impl Add for Quantity {
|
|
type Output = Self;
|
|
fn add(self, rhs: Self) -> Self::Output {
|
|
Self {
|
|
value: self.value.saturating_add(rhs.value),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Sub for Quantity {
|
|
type Output = Self;
|
|
fn sub(self, rhs: Self) -> Self::Output {
|
|
Self {
|
|
value: self.value.saturating_sub(rhs.value),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Mul<f64> for Quantity {
|
|
#[allow(clippy::float_arithmetic)]
|
|
type Output = Result<Self, CommonTypeError>;
|
|
fn mul(self, rhs: f64) -> Self::Output {
|
|
Self::from_f64(self.to_f64() * rhs)
|
|
}
|
|
}
|
|
|
|
impl Div<f64> for Quantity {
|
|
#[allow(clippy::float_arithmetic)]
|
|
type Output = Result<Self, CommonTypeError>;
|
|
fn div(self, rhs: f64) -> Self::Output {
|
|
if rhs == 0.0_f64 {
|
|
return Err(CommonTypeError::ConversionError {
|
|
message: "Cannot divide quantity by zero".to_owned(),
|
|
});
|
|
}
|
|
Self::from_f64(self.to_f64() / rhs)
|
|
}
|
|
}
|
|
|
|
impl Sum for Quantity {
|
|
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
|
|
iter.fold(Self::ZERO, |acc, x| Self { value: acc.value.saturating_add(x.value) })
|
|
}
|
|
}
|
|
|
|
impl<'quantity> Sum<&'quantity Self> for Quantity {
|
|
fn sum<I: Iterator<Item = &'quantity Self>>(iter: I) -> Self {
|
|
iter.fold(Self::ZERO, |acc, x| Self { value: acc.value.saturating_add(x.value) })
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// SQLX IMPLEMENTATIONS FOR FINANCIAL TYPES
|
|
// =============================================================================
|
|
|
|
#[cfg(feature = "database")]
|
|
mod sqlx_impls {
|
|
use super::{HftTimestamp, MarketRegime, OrderSide, OrderStatus, OrderType, Price, Quantity};
|
|
use rust_decimal::Decimal as RustDecimal;
|
|
use sqlx::{
|
|
decode::Decode,
|
|
encode::{Encode, IsNull},
|
|
error::BoxDynError,
|
|
postgres::{PgArgumentBuffer, PgTypeInfo, PgValueRef, Postgres},
|
|
Type,
|
|
};
|
|
|
|
// SQLx implementations for Price
|
|
impl Type<Postgres> for Price {
|
|
fn type_info() -> PgTypeInfo {
|
|
PgTypeInfo::with_name("NUMERIC")
|
|
}
|
|
}
|
|
|
|
impl<'query> Encode<'query, Postgres> for Price {
|
|
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
|
|
// Convert our fixed-point u64 to rust_decimal::Decimal with 8 decimal places
|
|
let decimal_value = RustDecimal::new(i64::try_from(self.raw_value()).unwrap_or(0), 8);
|
|
decimal_value.encode_by_ref(buf)
|
|
}
|
|
}
|
|
|
|
impl<'row> Decode<'row, Postgres> for Price {
|
|
fn decode(value: PgValueRef<'row>) -> Result<Self, BoxDynError> {
|
|
// Decode from NUMERIC to rust_decimal::Decimal
|
|
let decimal_value = <RustDecimal as Decode<Postgres>>::decode(value)?;
|
|
|
|
// Validate scale matches our fixed-point precision (8 decimal places)
|
|
if decimal_value.scale() != 8 {
|
|
return Err(format!(
|
|
"Invalid scale for Price: expected 8, got {}",
|
|
decimal_value.scale()
|
|
)
|
|
.into());
|
|
}
|
|
|
|
// Extract mantissa and convert to our u64 representation
|
|
let mantissa = decimal_value.mantissa();
|
|
let inner_val = u64::try_from(mantissa)
|
|
.map_err(|e| format!("Failed to convert negative or overflowing NUMERIC to Price: {}", e))?;
|
|
|
|
Ok(Price::from_raw(inner_val))
|
|
}
|
|
}
|
|
// SQLx implementations for Quantity
|
|
impl Type<Postgres> for Quantity {
|
|
fn type_info() -> PgTypeInfo {
|
|
PgTypeInfo::with_name("NUMERIC")
|
|
}
|
|
}
|
|
|
|
impl<'query> Encode<'query, Postgres> for Quantity {
|
|
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
|
|
// Convert our fixed-point u64 to rust_decimal::Decimal with 8 decimal places
|
|
let decimal_value = RustDecimal::new(i64::try_from(self.raw_value()).unwrap_or(0), 8);
|
|
decimal_value.encode_by_ref(buf)
|
|
}
|
|
}
|
|
|
|
impl<'row> Decode<'row, Postgres> for Quantity {
|
|
fn decode(value: PgValueRef<'row>) -> Result<Self, BoxDynError> {
|
|
// Decode from NUMERIC to rust_decimal::Decimal
|
|
let decimal_value = <RustDecimal as Decode<Postgres>>::decode(value)?;
|
|
|
|
// Validate scale matches our fixed-point precision (8 decimal places)
|
|
if decimal_value.scale() != 8 {
|
|
return Err(format!(
|
|
"Invalid scale for Quantity: expected 8, got {}",
|
|
decimal_value.scale()
|
|
)
|
|
.into());
|
|
}
|
|
|
|
// Extract mantissa and convert to our u64 representation
|
|
let mantissa = decimal_value.mantissa();
|
|
let inner_val = u64::try_from(mantissa)
|
|
.map_err(|e| format!("Failed to convert negative or overflowing NUMERIC to Quantity: {}", e))?;
|
|
|
|
Ok(Quantity::from_raw(inner_val))
|
|
}
|
|
}
|
|
|
|
// SQLx implementations for TimeInForce
|
|
impl Type<Postgres> for super::TimeInForce {
|
|
fn type_info() -> PgTypeInfo {
|
|
PgTypeInfo::with_name("TEXT")
|
|
}
|
|
}
|
|
|
|
impl<'query> Encode<'query, Postgres> for super::TimeInForce {
|
|
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
|
|
// Use the Display trait to convert enum to string representation
|
|
<&str as Encode<Postgres>>::encode(self.to_string().as_str(), buf)
|
|
}
|
|
}
|
|
|
|
impl<'row> Decode<'row, Postgres> for super::TimeInForce {
|
|
fn decode(value: PgValueRef<'row>) -> Result<Self, BoxDynError> {
|
|
// Decode from TEXT to string, then parse to enum
|
|
let s = <&str as Decode<Postgres>>::decode(value)?;
|
|
match s {
|
|
"DAY" => Ok(super::TimeInForce::Day),
|
|
"GTC" => Ok(super::TimeInForce::GoodTillCancel),
|
|
"IOC" => Ok(super::TimeInForce::ImmediateOrCancel),
|
|
"FOK" => Ok(super::TimeInForce::FillOrKill),
|
|
_ => Err(format!("Invalid TimeInForce value: {}", s).into()),
|
|
}
|
|
}
|
|
}
|
|
|
|
// SQLx implementations for OrderStatus
|
|
impl Type<Postgres> for OrderStatus {
|
|
fn type_info() -> PgTypeInfo {
|
|
PgTypeInfo::with_name("TEXT")
|
|
}
|
|
}
|
|
|
|
impl<'query> Encode<'query, Postgres> for OrderStatus {
|
|
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
|
|
let value = match self {
|
|
OrderStatus::Created => "CREATED",
|
|
OrderStatus::Submitted => "SUBMITTED",
|
|
OrderStatus::PartiallyFilled => "PARTIALLY_FILLED",
|
|
OrderStatus::Filled => "FILLED",
|
|
OrderStatus::Rejected => "REJECTED",
|
|
OrderStatus::Cancelled => "CANCELLED",
|
|
OrderStatus::New => "NEW",
|
|
OrderStatus::Expired => "EXPIRED",
|
|
OrderStatus::Pending => "PENDING",
|
|
OrderStatus::Working => "WORKING",
|
|
OrderStatus::Unknown => "UNKNOWN",
|
|
OrderStatus::Suspended => "SUSPENDED",
|
|
OrderStatus::PendingCancel => "PENDING_CANCEL",
|
|
OrderStatus::PendingReplace => "PENDING_REPLACE",
|
|
};
|
|
<&str as Encode<Postgres>>::encode_by_ref(&value, buf)
|
|
}
|
|
}
|
|
|
|
impl<'row> Decode<'row, Postgres> for OrderStatus {
|
|
fn decode(value: PgValueRef<'row>) -> Result<Self, BoxDynError> {
|
|
let s = <String as Decode<Postgres>>::decode(value)?;
|
|
match s.as_str() {
|
|
"CREATED" => Ok(OrderStatus::Created),
|
|
"SUBMITTED" => Ok(OrderStatus::Submitted),
|
|
"PARTIALLY_FILLED" => Ok(OrderStatus::PartiallyFilled),
|
|
"FILLED" => Ok(OrderStatus::Filled),
|
|
"REJECTED" => Ok(OrderStatus::Rejected),
|
|
"CANCELLED" => Ok(OrderStatus::Cancelled),
|
|
"NEW" => Ok(OrderStatus::New),
|
|
"EXPIRED" => Ok(OrderStatus::Expired),
|
|
"PENDING" => Ok(OrderStatus::Pending),
|
|
"WORKING" => Ok(OrderStatus::Working),
|
|
"UNKNOWN" => Ok(OrderStatus::Unknown),
|
|
"SUSPENDED" => Ok(OrderStatus::Suspended),
|
|
"PENDING_CANCEL" => Ok(OrderStatus::PendingCancel),
|
|
"PENDING_REPLACE" => Ok(OrderStatus::PendingReplace),
|
|
_ => Err(format!("Invalid OrderStatus value: {}", s).into()),
|
|
}
|
|
}
|
|
}
|
|
|
|
// SQLx implementations for OrderSide
|
|
impl Type<Postgres> for OrderSide {
|
|
fn type_info() -> PgTypeInfo {
|
|
PgTypeInfo::with_name("TEXT")
|
|
}
|
|
}
|
|
|
|
impl<'query> Encode<'query, Postgres> for OrderSide {
|
|
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
|
|
let value = match self {
|
|
OrderSide::Buy => "BUY",
|
|
OrderSide::Sell => "SELL",
|
|
};
|
|
<&str as Encode<Postgres>>::encode_by_ref(&value, buf)
|
|
}
|
|
}
|
|
|
|
impl<'row> Decode<'row, Postgres> for OrderSide {
|
|
fn decode(value: PgValueRef<'row>) -> Result<Self, BoxDynError> {
|
|
let s = <String as Decode<Postgres>>::decode(value)?;
|
|
match s.as_str() {
|
|
"BUY" => Ok(OrderSide::Buy),
|
|
"SELL" => Ok(OrderSide::Sell),
|
|
_ => Err(format!("Invalid OrderSide value: {}", s).into()),
|
|
}
|
|
}
|
|
}
|
|
|
|
// SQLx implementations for OrderType
|
|
impl Type<Postgres> for OrderType {
|
|
fn type_info() -> PgTypeInfo {
|
|
PgTypeInfo::with_name("TEXT")
|
|
}
|
|
}
|
|
|
|
impl<'query> Encode<'query, Postgres> for OrderType {
|
|
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
|
|
let value = match self {
|
|
OrderType::Market => "MARKET",
|
|
OrderType::Limit => "LIMIT",
|
|
OrderType::Stop => "STOP",
|
|
OrderType::StopLimit => "STOP_LIMIT",
|
|
OrderType::Iceberg => "ICEBERG",
|
|
OrderType::TrailingStop => "TRAILING_STOP",
|
|
OrderType::Hidden => "HIDDEN",
|
|
};
|
|
<&str as Encode<Postgres>>::encode_by_ref(&value, buf)
|
|
}
|
|
}
|
|
|
|
impl<'row> Decode<'row, Postgres> for OrderType {
|
|
fn decode(value: PgValueRef<'row>) -> Result<Self, BoxDynError> {
|
|
let s = <String as Decode<Postgres>>::decode(value)?;
|
|
match s.as_str() {
|
|
"MARKET" => Ok(OrderType::Market),
|
|
"LIMIT" => Ok(OrderType::Limit),
|
|
"STOP" => Ok(OrderType::Stop),
|
|
"STOP_LIMIT" => Ok(OrderType::StopLimit),
|
|
"ICEBERG" => Ok(OrderType::Iceberg),
|
|
"TRAILING_STOP" => Ok(OrderType::TrailingStop),
|
|
"HIDDEN" => Ok(OrderType::Hidden),
|
|
_ => Err(format!("Invalid OrderType value: {}", s).into()),
|
|
}
|
|
}
|
|
}
|
|
|
|
// SQLx implementations for MarketRegime
|
|
impl Type<Postgres> for MarketRegime {
|
|
fn type_info() -> PgTypeInfo {
|
|
PgTypeInfo::with_name("TEXT")
|
|
}
|
|
}
|
|
|
|
impl<'query> Encode<'query, Postgres> for MarketRegime {
|
|
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
|
|
let value = match self {
|
|
MarketRegime::Normal => "NORMAL",
|
|
MarketRegime::Crisis => "CRISIS",
|
|
MarketRegime::Trending => "TRENDING",
|
|
MarketRegime::Sideways => "SIDEWAYS",
|
|
MarketRegime::Bull => "BULL",
|
|
MarketRegime::Bear => "BEAR",
|
|
MarketRegime::HighVolatility => "HIGH_VOLATILITY",
|
|
MarketRegime::LowVolatility => "LOW_VOLATILITY",
|
|
MarketRegime::Volatile => "VOLATILE",
|
|
MarketRegime::Calm => "CALM",
|
|
MarketRegime::Unknown => "UNKNOWN",
|
|
MarketRegime::Recovery => "RECOVERY",
|
|
MarketRegime::Bubble => "BUBBLE",
|
|
MarketRegime::Correction => "CORRECTION",
|
|
MarketRegime::Custom(id) => {
|
|
return <String as Encode<Postgres>>::encode_by_ref(
|
|
&format!("CUSTOM_{}", id),
|
|
buf,
|
|
)
|
|
},
|
|
};
|
|
<&str as Encode<Postgres>>::encode_by_ref(&value, buf)
|
|
}
|
|
}
|
|
|
|
impl<'row> Decode<'row, Postgres> for MarketRegime {
|
|
fn decode(value: PgValueRef<'row>) -> Result<Self, BoxDynError> {
|
|
let s = <String as Decode<Postgres>>::decode(value)?;
|
|
match s.as_str() {
|
|
"NORMAL" => Ok(MarketRegime::Normal),
|
|
"CRISIS" => Ok(MarketRegime::Crisis),
|
|
"TRENDING" => Ok(MarketRegime::Trending),
|
|
"SIDEWAYS" => Ok(MarketRegime::Sideways),
|
|
"BULL" => Ok(MarketRegime::Bull),
|
|
"BEAR" => Ok(MarketRegime::Bear),
|
|
"HIGH_VOLATILITY" => Ok(MarketRegime::HighVolatility),
|
|
"LOW_VOLATILITY" => Ok(MarketRegime::LowVolatility),
|
|
"VOLATILE" => Ok(MarketRegime::Volatile),
|
|
"CALM" => Ok(MarketRegime::Calm),
|
|
"UNKNOWN" => Ok(MarketRegime::Unknown),
|
|
"RECOVERY" => Ok(MarketRegime::Recovery),
|
|
"BUBBLE" => Ok(MarketRegime::Bubble),
|
|
"CORRECTION" => Ok(MarketRegime::Correction),
|
|
_ => {
|
|
// Handle Custom(id) format
|
|
if let Some(id_str) = s.strip_prefix("CUSTOM_") {
|
|
if let Ok(id) = id_str.parse::<usize>() {
|
|
Ok(MarketRegime::Custom(id))
|
|
} else {
|
|
Err(format!("Invalid MarketRegime Custom ID: {}", id_str).into())
|
|
}
|
|
} else {
|
|
Err(format!("Invalid MarketRegime value: {}", s).into())
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
// SQLx implementations for HftTimestamp
|
|
// Maps to PostgreSQL BIGINT (stores nanoseconds since Unix epoch)
|
|
// Note: Limited to i64::MAX nanoseconds (year 2262) due to PostgreSQL BIGINT constraints
|
|
impl<'query> Encode<'query, Postgres> for HftTimestamp {
|
|
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
|
|
// Cast u64 to i64 for PostgreSQL BIGINT compatibility
|
|
<i64 as Encode<Postgres>>::encode(i64::try_from(self.nanos()).unwrap_or(0), buf)
|
|
}
|
|
}
|
|
|
|
impl<'row> Decode<'row, Postgres> for HftTimestamp {
|
|
fn decode(value: PgValueRef<'row>) -> Result<Self, BoxDynError> {
|
|
let val = <i64 as Decode<Postgres>>::decode(value)?;
|
|
// Cast i64 back to u64 for internal representation
|
|
Ok(HftTimestamp::from_nanos(u64::try_from(val).unwrap_or(0)))
|
|
}
|
|
}
|
|
|
|
impl Type<Postgres> for HftTimestamp {
|
|
fn type_info() -> <Postgres as sqlx::Database>::TypeInfo {
|
|
<i64 as Type<Postgres>>::type_info()
|
|
}
|
|
|
|
fn compatible(ty: &<Postgres as sqlx::Database>::TypeInfo) -> bool {
|
|
<i64 as Type<Postgres>>::compatible(ty)
|
|
}
|
|
}
|
|
|
|
// SQLx implementations for OrderId (uses BIGINT for u64)
|
|
impl Type<Postgres> for super::OrderId {
|
|
fn type_info() -> PgTypeInfo {
|
|
PgTypeInfo::with_name("BIGINT")
|
|
}
|
|
}
|
|
|
|
impl<'query> Encode<'query, Postgres> for super::OrderId {
|
|
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
|
|
<i64 as Encode<Postgres>>::encode_by_ref(&(i64::try_from(self.value()).unwrap_or(0)), buf)
|
|
}
|
|
}
|
|
|
|
impl<'row> Decode<'row, Postgres> for super::OrderId {
|
|
fn decode(value: PgValueRef<'row>) -> Result<Self, BoxDynError> {
|
|
let id = <i64 as Decode<Postgres>>::decode(value)?;
|
|
Ok(super::OrderId::from_u64(u64::try_from(id).unwrap_or(0)))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Volume type - alias for Quantity with the same fixed-point arithmetic
|
|
///
|
|
/// `SQLx` traits are automatically inherited from Quantity
|
|
pub type Volume = Quantity;
|
|
|
|
// ORDER TYPES ALREADY DEFINED ABOVE - No need to re-export from trading_engine
|
|
// =============================================================================
|
|
// CORE ID TYPES (MOVED FROM TRADING_ENGINE)
|
|
// =============================================================================
|
|
|
|
/// Order identifier with ultra-fast atomic generation
|
|
///
|
|
/// Replaces slow UUID generation (1ms+) with atomic increment (~5ns)
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
|
pub struct OrderId(u64);
|
|
|
|
impl Default for OrderId {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl OrderId {
|
|
/// Generate next `OrderId` using atomic counter - <50ns performance
|
|
pub fn new() -> Self {
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
static COUNTER: AtomicU64 = AtomicU64::new(1);
|
|
Self(COUNTER.fetch_add(1, Ordering::Relaxed))
|
|
}
|
|
|
|
/// Create `OrderId` from u64 value
|
|
#[must_use]
|
|
pub const fn from_u64(value: u64) -> Self {
|
|
Self(value)
|
|
}
|
|
|
|
/// Get u64 value
|
|
#[must_use]
|
|
pub const fn value(&self) -> u64 {
|
|
self.0
|
|
}
|
|
|
|
/// Get u64 value for performance-critical code (alias for value)
|
|
#[must_use]
|
|
pub const fn as_u64(&self) -> u64 {
|
|
self.0
|
|
}
|
|
|
|
/// Get as string for compatibility
|
|
#[must_use]
|
|
pub fn as_str(&self) -> String {
|
|
self.0.to_string()
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for OrderId {
|
|
/// Format the order ID for display
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.0)
|
|
}
|
|
}
|
|
|
|
impl From<u64> for OrderId {
|
|
/// Create an `OrderId` from a u64 value
|
|
fn from(value: u64) -> Self {
|
|
Self(value)
|
|
}
|
|
}
|
|
|
|
impl From<OrderId> for u64 {
|
|
/// Convert an `OrderId` to u64
|
|
fn from(order_id: OrderId) -> Self {
|
|
order_id.0
|
|
}
|
|
}
|
|
|
|
impl FromStr for OrderId {
|
|
type Err = ParseIntError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
s.parse::<u64>().map(OrderId)
|
|
}
|
|
}
|
|
|
|
impl From<String> for OrderId {
|
|
/// Create an `OrderId` from a String, generating new ID if parsing fails
|
|
fn from(s: String) -> Self {
|
|
s.parse().unwrap_or_else(|_| Self::new())
|
|
}
|
|
}
|
|
|
|
impl From<&str> for OrderId {
|
|
/// Create an `OrderId` from a &str, generating new ID if parsing fails
|
|
fn from(s: &str) -> Self {
|
|
s.parse().unwrap_or_else(|_| Self::new())
|
|
}
|
|
}
|
|
|
|
/// Execution identifier with validation
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[cfg_attr(feature = "database", derive(sqlx::Type))]
|
|
pub struct ExecutionId(String);
|
|
|
|
impl ExecutionId {
|
|
/// Create a new execution ID with validation
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
|
|
let id = id.into();
|
|
if id.trim().is_empty() {
|
|
return Err(CommonTypeError::ValidationError {
|
|
field: "execution_id".to_owned(),
|
|
reason: "Execution ID cannot be empty".to_owned(),
|
|
});
|
|
}
|
|
Ok(Self(id))
|
|
}
|
|
|
|
/// Generate a new random execution ID
|
|
pub fn generate() -> Self {
|
|
Self(uuid::Uuid::new_v4().to_string())
|
|
}
|
|
|
|
/// Get execution ID as string slice
|
|
pub fn as_str(&self) -> &str {
|
|
&self.0
|
|
}
|
|
|
|
/// Convert execution ID into owned string
|
|
pub fn into_string(self) -> String {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for ExecutionId {
|
|
/// Format the execution ID for display
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.0)
|
|
}
|
|
}
|
|
|
|
impl FromStr for ExecutionId {
|
|
type Err = CommonTypeError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Self::new(s)
|
|
}
|
|
}
|
|
|
|
/// Trade identifier with validation
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub struct TradeId(String);
|
|
|
|
impl TradeId {
|
|
/// Create a new trade ID with validation
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
|
|
let id = id.into();
|
|
if id.is_empty() {
|
|
return Err(CommonTypeError::ValidationError {
|
|
field: "trade_id".to_owned(),
|
|
reason: "Trade ID cannot be empty".to_owned(),
|
|
});
|
|
}
|
|
Ok(Self(id))
|
|
}
|
|
|
|
/// Get the trade ID as a string slice
|
|
pub fn as_str(&self) -> &str {
|
|
&self.0
|
|
}
|
|
/// Convert the trade ID into an owned string
|
|
pub fn into_string(self) -> String {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for TradeId {
|
|
/// Format the trade ID for display
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.0)
|
|
}
|
|
}
|
|
|
|
/// Trading symbol with validation
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[cfg_attr(feature = "database", derive(sqlx::Type))]
|
|
pub struct Symbol {
|
|
value: String,
|
|
}
|
|
|
|
impl Symbol {
|
|
/// Create a new symbol from a string
|
|
#[must_use]
|
|
pub const fn new(s: String) -> Self {
|
|
Self { value: s }
|
|
}
|
|
|
|
/// Create a new Symbol with validation
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn new_validated(s: String) -> Result<Self, CommonTypeError> {
|
|
if s.trim().is_empty() {
|
|
return Err(CommonTypeError::ValidationError {
|
|
field: "symbol".to_owned(),
|
|
reason: "Symbol cannot be empty".to_owned(),
|
|
});
|
|
}
|
|
Ok(Self { value: s })
|
|
}
|
|
|
|
/// Create a Symbol from &str with validation
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn from_str_validated(s: &str) -> Result<Self, CommonTypeError> {
|
|
Self::new_validated(s.to_owned())
|
|
}
|
|
|
|
/// Get the symbol as a string slice
|
|
#[must_use]
|
|
pub fn as_str(&self) -> &str {
|
|
&self.value
|
|
}
|
|
/// Get the symbol value as a string slice
|
|
#[must_use]
|
|
pub fn value(&self) -> &str {
|
|
&self.value
|
|
}
|
|
/// Get the symbol as bytes
|
|
#[must_use]
|
|
pub fn as_bytes(&self) -> &[u8] {
|
|
self.value.as_bytes()
|
|
}
|
|
/// Check if the symbol is empty
|
|
#[must_use]
|
|
pub fn is_empty(&self) -> bool {
|
|
self.value.is_empty()
|
|
}
|
|
/// Convert the symbol to uppercase
|
|
#[must_use]
|
|
pub fn to_uppercase(&self) -> String {
|
|
self.value.to_uppercase()
|
|
}
|
|
/// Replace occurrences in the symbol
|
|
#[must_use]
|
|
pub fn replace(&self, from: &str, to: &str) -> String {
|
|
self.value.replace(from, to)
|
|
}
|
|
|
|
/// Helper for risk management - creates a 'NONE' symbol
|
|
#[must_use]
|
|
pub fn none() -> Self {
|
|
"NONE".parse().unwrap()
|
|
}
|
|
|
|
/// Check if the symbol contains a pattern
|
|
#[must_use]
|
|
pub fn contains(&self, pattern: &str) -> bool {
|
|
self.value.contains(pattern)
|
|
}
|
|
}
|
|
|
|
impl FromStr for Symbol {
|
|
type Err = std::convert::Infallible;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Ok(Self {
|
|
value: s.to_owned(),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Additional implementation to support conversion from &Symbol to &str
|
|
impl AsRef<str> for Symbol {
|
|
/// Convert symbol to string reference
|
|
fn as_ref(&self) -> &str {
|
|
&self.value
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Symbol {
|
|
/// Format the symbol for display
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.value)
|
|
}
|
|
}
|
|
|
|
impl From<String> for Symbol {
|
|
/// Create a Symbol from a String
|
|
fn from(s: String) -> Self {
|
|
Self::new(s)
|
|
}
|
|
}
|
|
impl From<&str> for Symbol {
|
|
/// Create a Symbol from a &str
|
|
fn from(s: &str) -> Self {
|
|
Self::new(s.to_owned())
|
|
}
|
|
}
|
|
|
|
// TryFrom implementations removed due to conflicting blanket implementations
|
|
// Use Symbol::new_validated() or Symbol::from_validated() directly instead
|
|
|
|
impl Default for Symbol {
|
|
/// Returns the default symbol (empty string)
|
|
fn default() -> Self {
|
|
Self::new(String::new())
|
|
}
|
|
}
|
|
|
|
impl PartialEq<str> for Symbol {
|
|
fn eq(&self, other: &str) -> bool {
|
|
self.value == other
|
|
}
|
|
}
|
|
|
|
impl PartialEq<&str> for Symbol {
|
|
fn eq(&self, other: &&str) -> bool {
|
|
self.value == *other
|
|
}
|
|
}
|
|
|
|
impl PartialEq<String> for Symbol {
|
|
fn eq(&self, other: &String) -> bool {
|
|
&self.value == other
|
|
}
|
|
}
|
|
|
|
impl PartialEq<Symbol> for &str {
|
|
fn eq(&self, other: &Symbol) -> bool {
|
|
*self == other.value
|
|
}
|
|
}
|
|
|
|
impl PartialEq<Symbol> for String {
|
|
fn eq(&self, other: &Symbol) -> bool {
|
|
self == &other.value
|
|
}
|
|
}
|
|
|
|
// TimeInForce moved to canonical source: common::types::TimeInForce
|
|
|
|
// Currency moved to canonical source: common::types::Currency
|
|
|
|
// Price moved to canonical source: common::types::Price
|
|
|
|
// Quantity moved to canonical source: common::types::Quantity
|
|
// Volume moved to canonical source: common::types::Quantity (as Volume alias)
|
|
|
|
/// Money amount with currency
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct Money {
|
|
/// The monetary amount
|
|
pub amount: Decimal,
|
|
/// The currency of the amount
|
|
pub currency: Currency,
|
|
}
|
|
|
|
impl Money {
|
|
/// Create new money amount
|
|
pub const fn new(amount: Decimal, currency: Currency) -> Self {
|
|
Self { amount, currency }
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Money {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{} {}", self.amount, self.currency)
|
|
}
|
|
}
|
|
|
|
// OrderId moved to canonical source: common::types::OrderId
|
|
|
|
// TradeId moved to canonical source: common::types::TradeId
|
|
|
|
// Symbol moved to canonical source: common::types::Symbol
|
|
|
|
/// Type-safe account identifier
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub struct AccountId(String);
|
|
|
|
impl AccountId {
|
|
/// Create a new account ID with validation
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
|
|
let id = id.into();
|
|
if id.trim().is_empty() {
|
|
return Err(CommonTypeError::InvalidIdentifier {
|
|
field: "account_id".to_owned(),
|
|
reason: "Account ID cannot be empty".to_owned(),
|
|
});
|
|
}
|
|
Ok(Self(id))
|
|
}
|
|
|
|
/// Get the ID as a string slice
|
|
pub fn as_str(&self) -> &str {
|
|
&self.0
|
|
}
|
|
|
|
/// Convert to owned String
|
|
pub fn into_string(self) -> String {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for AccountId {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.0)
|
|
}
|
|
}
|
|
|
|
/// High-precision timestamp for HFT applications - CANONICAL DEFINITION
|
|
///
|
|
/// Robust implementation with error handling for financial safety
|
|
#[derive(
|
|
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
|
|
)]
|
|
pub struct HftTimestamp {
|
|
nanos: u64,
|
|
}
|
|
|
|
impl HftTimestamp {
|
|
/// Get current timestamp with error handling for financial safety
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn now() -> Result<Self, CommonError> {
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
let nanos = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map_err(|e| CommonError::Service {
|
|
category: CommonErrorCategory::System,
|
|
message: format!("System time before UNIX epoch: {e}"),
|
|
})?
|
|
.as_nanos().try_into().unwrap_or(0_u64);
|
|
Ok(Self { nanos })
|
|
}
|
|
|
|
/// Get current timestamp with error handling for financial safety (`CommonTypeError` version)
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn now_common() -> Result<Self, CommonTypeError> {
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
let nanos = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map_err(|e| CommonTypeError::ConversionError {
|
|
message: format!("System time before UNIX epoch: {e}"),
|
|
})?
|
|
.as_nanos().try_into().unwrap_or(0_u64);
|
|
Ok(Self { nanos })
|
|
}
|
|
|
|
/// Get current timestamp or zero if system time is invalid
|
|
#[must_use]
|
|
pub fn now_or_zero() -> Self {
|
|
Self::now().unwrap_or(Self { nanos: 0 })
|
|
}
|
|
|
|
/// Get nanoseconds since epoch
|
|
#[must_use]
|
|
pub const fn nanos(self) -> u64 {
|
|
self.nanos
|
|
}
|
|
|
|
#[allow(clippy::as_conversions)]
|
|
/// Create from nanoseconds since epoch
|
|
#[must_use]
|
|
pub const fn from_nanos(nanos: u64) -> Self {
|
|
Self { nanos }
|
|
}
|
|
|
|
/// Create from signed nanoseconds (cast to unsigned)
|
|
#[must_use]
|
|
#[allow(clippy::as_conversions)]
|
|
pub const fn from_nanos_i64(nanos: i64) -> Self {
|
|
Self {
|
|
nanos: nanos as u64,
|
|
}
|
|
}
|
|
|
|
/// Get nanoseconds since epoch
|
|
pub const fn as_nanos(&self) -> u64 {
|
|
self.nanos
|
|
}
|
|
|
|
/// Convert to `DateTime<Utc>`
|
|
#[allow(clippy::integer_division)]
|
|
pub fn to_datetime(&self) -> DateTime<Utc> {
|
|
let secs = self.nanos / 1_000_000_000;
|
|
let nsecs = u32::try_from(self.nanos % 1_000_000_000).unwrap_or(0);
|
|
DateTime::from_timestamp(i64::try_from(secs).unwrap_or(0), nsecs).unwrap_or_default()
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for HftTimestamp {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.to_datetime())
|
|
}
|
|
}
|
|
|
|
/// Generic timestamp for general use cases
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
|
pub struct GenericTimestamp {
|
|
nanos: u64,
|
|
}
|
|
|
|
impl GenericTimestamp {
|
|
/// Create from nanoseconds since epoch
|
|
#[must_use]
|
|
pub const fn from_nanos(nanos: u64) -> Self {
|
|
Self { nanos }
|
|
}
|
|
|
|
/// Get nanoseconds since epoch
|
|
#[must_use]
|
|
pub const fn nanos(&self) -> u64 {
|
|
self.nanos
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// MARKET TYPES (MIGRATED FROM TRADING_ENGINE)
|
|
// =============================================================================
|
|
|
|
/// Market regime enumeration for position sizing scaling and risk management
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum MarketRegime {
|
|
/// Normal market conditions
|
|
Normal,
|
|
/// Crisis/stress market conditions
|
|
Crisis,
|
|
/// Trending market (strong directional movement)
|
|
Trending,
|
|
/// Sideways/ranging market (low volatility)
|
|
Sideways,
|
|
/// Bull market (sustained upward trend)
|
|
Bull,
|
|
/// Bear market (sustained downward trend)
|
|
Bear,
|
|
/// High volatility market conditions
|
|
HighVolatility,
|
|
/// Low volatility market conditions
|
|
LowVolatility,
|
|
/// Volatile market conditions (alias for `HighVolatility`)
|
|
Volatile,
|
|
/// Calm market conditions (alias for `LowVolatility`)
|
|
Calm,
|
|
/// Unknown/unclassified regime
|
|
Unknown,
|
|
/// Recovery regime - transitioning from crisis
|
|
Recovery,
|
|
/// Bubble regime - unsustainable upward movement
|
|
Bubble,
|
|
/// Correction regime - temporary downward adjustment
|
|
Correction,
|
|
/// Custom regime with numeric identifier
|
|
Custom(usize),
|
|
}
|
|
|
|
impl Default for MarketRegime {
|
|
fn default() -> Self {
|
|
Self::Normal
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for MarketRegime {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Normal => write!(f, "Normal"),
|
|
Self::Crisis => write!(f, "Crisis"),
|
|
Self::Trending => write!(f, "Trending"),
|
|
Self::Sideways => write!(f, "Sideways"),
|
|
Self::Bull => write!(f, "Bull"),
|
|
Self::Bear => write!(f, "Bear"),
|
|
Self::HighVolatility => write!(f, "HighVolatility"),
|
|
Self::LowVolatility => write!(f, "LowVolatility"),
|
|
Self::Volatile => write!(f, "Volatile"),
|
|
Self::Calm => write!(f, "Calm"),
|
|
Self::Unknown => write!(f, "Unknown"),
|
|
Self::Recovery => write!(f, "Recovery"),
|
|
Self::Bubble => write!(f, "Bubble"),
|
|
Self::Correction => write!(f, "Correction"),
|
|
Self::Custom(id) => write!(f, "Custom({id})"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Tick type enumeration for market data
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[cfg_attr(feature = "database", derive(sqlx::Type))]
|
|
#[cfg_attr(
|
|
feature = "database",
|
|
sqlx(type_name = "tick_type", rename_all = "snake_case")
|
|
)]
|
|
pub enum TickType {
|
|
/// Trade execution tick
|
|
Trade,
|
|
/// Bid price update tick
|
|
Bid,
|
|
/// Ask price update tick
|
|
Ask,
|
|
/// Quote (bid/ask) update tick
|
|
Quote,
|
|
}
|
|
|
|
/// Exchange enumeration for trading venues
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum Exchange {
|
|
/// New York Stock Exchange
|
|
NYSE,
|
|
/// NASDAQ
|
|
NASDAQ,
|
|
/// Chicago Mercantile Exchange
|
|
CME,
|
|
/// Intercontinental Exchange
|
|
ICE,
|
|
/// London Stock Exchange
|
|
LSE,
|
|
/// Tokyo Stock Exchange
|
|
TSE,
|
|
/// Hong Kong Stock Exchange
|
|
HKEX,
|
|
/// Shanghai Stock Exchange
|
|
SSE,
|
|
/// Shenzhen Stock Exchange
|
|
SZSE,
|
|
/// Euronext
|
|
EURONEXT,
|
|
/// Deutsche Börse
|
|
XETRA,
|
|
/// Chicago Board of Trade
|
|
CBOT,
|
|
/// Chicago Board Options Exchange
|
|
CBOE,
|
|
/// BATS Global Markets
|
|
BATS,
|
|
/// IEX Exchange
|
|
IEX,
|
|
/// Interactive Brokers
|
|
IBKR,
|
|
/// IC Markets
|
|
ICMARKETS,
|
|
/// Forex.com
|
|
FOREX,
|
|
/// Binance
|
|
BINANCE,
|
|
/// Coinbase
|
|
COINBASE,
|
|
/// Kraken
|
|
KRAKEN,
|
|
/// Unknown or unrecognized exchange
|
|
UNKNOWN,
|
|
}
|
|
|
|
impl Default for Exchange {
|
|
fn default() -> Self {
|
|
Self::UNKNOWN
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Exchange {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::NYSE => write!(f, "NYSE"),
|
|
Self::NASDAQ => write!(f, "NASDAQ"),
|
|
Self::CME => write!(f, "CME"),
|
|
Self::ICE => write!(f, "ICE"),
|
|
Self::LSE => write!(f, "LSE"),
|
|
Self::TSE => write!(f, "TSE"),
|
|
Self::HKEX => write!(f, "HKEX"),
|
|
Self::SSE => write!(f, "SSE"),
|
|
Self::SZSE => write!(f, "SZSE"),
|
|
Self::EURONEXT => write!(f, "EURONEXT"),
|
|
Self::XETRA => write!(f, "XETRA"),
|
|
Self::CBOT => write!(f, "CBOT"),
|
|
Self::CBOE => write!(f, "CBOE"),
|
|
Self::BATS => write!(f, "BATS"),
|
|
Self::IEX => write!(f, "IEX"),
|
|
Self::IBKR => write!(f, "IBKR"),
|
|
Self::ICMARKETS => write!(f, "ICMARKETS"),
|
|
Self::FOREX => write!(f, "FOREX"),
|
|
Self::BINANCE => write!(f, "BINANCE"),
|
|
Self::COINBASE => write!(f, "COINBASE"),
|
|
Self::KRAKEN => write!(f, "KRAKEN"),
|
|
Self::UNKNOWN => write!(f, "UNKNOWN"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FromStr for Exchange {
|
|
type Err = CommonTypeError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s.to_uppercase().as_str() {
|
|
"NYSE" => Ok(Self::NYSE),
|
|
"NASDAQ" => Ok(Self::NASDAQ),
|
|
"CME" => Ok(Self::CME),
|
|
"ICE" => Ok(Self::ICE),
|
|
"LSE" => Ok(Self::LSE),
|
|
"TSE" => Ok(Self::TSE),
|
|
"HKEX" => Ok(Self::HKEX),
|
|
"SSE" => Ok(Self::SSE),
|
|
"SZSE" => Ok(Self::SZSE),
|
|
"EURONEXT" => Ok(Self::EURONEXT),
|
|
"XETRA" => Ok(Self::XETRA),
|
|
"CBOT" => Ok(Self::CBOT),
|
|
"CBOE" => Ok(Self::CBOE),
|
|
"BATS" => Ok(Self::BATS),
|
|
"IEX" => Ok(Self::IEX),
|
|
"IBKR" => Ok(Self::IBKR),
|
|
"ICMARKETS" => Ok(Self::ICMARKETS),
|
|
"FOREX" => Ok(Self::FOREX),
|
|
"BINANCE" => Ok(Self::BINANCE),
|
|
"COINBASE" => Ok(Self::COINBASE),
|
|
"KRAKEN" => Ok(Self::KRAKEN),
|
|
"UNKNOWN" => Ok(Self::UNKNOWN),
|
|
_ => Ok(Self::UNKNOWN), // Default to UNKNOWN for unrecognized exchanges
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Market tick data structure - CANONICAL SINGLE SOURCE OF TRUTH
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct MarketTick {
|
|
/// Trading symbol
|
|
pub symbol: Symbol,
|
|
/// Tick price
|
|
pub price: Price,
|
|
/// Tick size/quantity
|
|
pub size: Quantity,
|
|
/// Tick timestamp
|
|
pub timestamp: HftTimestamp,
|
|
/// Type of tick (trade, bid, ask, quote)
|
|
pub tick_type: TickType,
|
|
/// Exchange where the tick occurred
|
|
pub exchange: Exchange,
|
|
/// Sequence number for ordering
|
|
pub sequence_number: u64,
|
|
}
|
|
|
|
impl MarketTick {
|
|
/// Create a new market tick with current timestamp
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn new(
|
|
symbol: Symbol,
|
|
price: Price,
|
|
size: Quantity,
|
|
tick_type: TickType,
|
|
exchange: Exchange,
|
|
sequence_number: u64,
|
|
) -> Result<Self, CommonError> {
|
|
Ok(Self {
|
|
symbol,
|
|
price,
|
|
size,
|
|
timestamp: HftTimestamp::now()?,
|
|
tick_type,
|
|
exchange,
|
|
sequence_number,
|
|
})
|
|
}
|
|
|
|
/// Create a new market tick with specified timestamp (for backtesting)
|
|
#[must_use]
|
|
pub const fn with_timestamp(
|
|
symbol: Symbol,
|
|
price: Price,
|
|
size: Quantity,
|
|
timestamp: HftTimestamp,
|
|
tick_type: TickType,
|
|
exchange: Exchange,
|
|
sequence_number: u64,
|
|
) -> Self {
|
|
Self {
|
|
symbol,
|
|
price,
|
|
size,
|
|
timestamp,
|
|
tick_type,
|
|
exchange,
|
|
sequence_number,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Trading signal for algorithmic trading
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct TradingSignal {
|
|
/// Signal ID
|
|
pub signal_id: Uuid,
|
|
/// Symbol this signal applies to
|
|
pub symbol: Symbol,
|
|
/// Signal strength (-1.0 to 1.0)
|
|
pub strength: f64,
|
|
/// Signal direction
|
|
pub direction: OrderSide,
|
|
/// Confidence level (0.0 to 1.0)
|
|
pub confidence: f64,
|
|
/// Signal generation timestamp
|
|
pub timestamp: HftTimestamp,
|
|
/// Signal source/strategy
|
|
pub source: String,
|
|
/// Additional metadata
|
|
pub metadata: std::collections::HashMap<String, String>,
|
|
}
|
|
|
|
impl TradingSignal {
|
|
/// Create a new trading signal
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn new(
|
|
symbol: Symbol,
|
|
strength: f64,
|
|
direction: OrderSide,
|
|
confidence: f64,
|
|
source: String,
|
|
) -> Result<Self, CommonTypeError> {
|
|
if !(0.0_f64..=1.0_f64).contains(&confidence) {
|
|
return Err(CommonTypeError::ValidationError {
|
|
field: "confidence".to_owned(),
|
|
reason: "Confidence must be between 0.0 and 1.0".to_owned(),
|
|
});
|
|
}
|
|
if !(-1.0_f64..=1.0_f64).contains(&strength) {
|
|
return Err(CommonTypeError::ValidationError {
|
|
field: "strength".to_owned(),
|
|
reason: "Strength must be between -1.0 and 1.0".to_owned(),
|
|
});
|
|
}
|
|
|
|
Ok(Self {
|
|
signal_id: Uuid::new_v4(),
|
|
symbol,
|
|
strength,
|
|
direction,
|
|
confidence,
|
|
timestamp: HftTimestamp::now_common()?,
|
|
source,
|
|
metadata: std::collections::HashMap::new(),
|
|
})
|
|
}
|
|
|
|
/// Add metadata to the signal
|
|
#[must_use]
|
|
pub fn with_metadata(mut self, key: String, value: String) -> Self {
|
|
self.metadata.insert(key, value);
|
|
self
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// HIGH-PERFORMANCE TYPES FOR COPY/CLONE OPTIMIZATION
|
|
// =============================================================================
|
|
|
|
/// Lightweight Order reference for high-performance contexts requiring Copy trait
|
|
///
|
|
/// This struct contains only the essential order data needed for performance-critical
|
|
/// operations like `SmallBatchRing` processing, while maintaining Copy semantics.
|
|
///
|
|
/// For full order details, use the complete Order struct.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub struct OrderRef {
|
|
/// Order ID (u64 for performance)
|
|
pub id: u64,
|
|
/// Symbol hash for fast lookups
|
|
pub symbol_hash: i64,
|
|
/// Order side (Buy/Sell)
|
|
pub side: OrderSide,
|
|
/// Order type
|
|
pub order_type: OrderType,
|
|
/// Quantity (fixed-point u64)
|
|
pub quantity: u64,
|
|
/// Price (fixed-point u64, 0 for market orders)
|
|
pub price: u64,
|
|
/// Timestamp (nanoseconds since epoch)
|
|
pub timestamp: u64,
|
|
}
|
|
|
|
impl OrderRef {
|
|
/// Create `OrderRef` from a full Order struct
|
|
#[must_use]
|
|
pub fn from_order(order: &Order) -> Self {
|
|
Self {
|
|
id: order.id.value(),
|
|
symbol_hash: order.symbol_hash(),
|
|
side: order.side,
|
|
order_type: order.order_type,
|
|
quantity: order.quantity.raw_value(),
|
|
price: order.price.map_or(0, |p| p.raw_value()),
|
|
timestamp: order.created_at.nanos(),
|
|
}
|
|
}
|
|
|
|
/// Create a limit order reference
|
|
#[must_use]
|
|
pub fn limit(symbol_hash: i64, side: OrderSide, quantity: u64, price: u64) -> Self {
|
|
Self {
|
|
id: OrderId::new().value(),
|
|
symbol_hash,
|
|
side,
|
|
order_type: OrderType::Limit,
|
|
quantity,
|
|
price,
|
|
timestamp: HftTimestamp::now_or_zero().nanos(),
|
|
}
|
|
}
|
|
|
|
/// Create a market order reference
|
|
#[must_use]
|
|
pub fn market(symbol_hash: i64, side: OrderSide, quantity: u64) -> Self {
|
|
Self {
|
|
id: OrderId::new().value(),
|
|
symbol_hash,
|
|
side,
|
|
order_type: OrderType::Market,
|
|
quantity,
|
|
price: 0,
|
|
timestamp: HftTimestamp::now_or_zero().nanos(),
|
|
}
|
|
}
|
|
|
|
/// Get quantity as Quantity type
|
|
#[must_use]
|
|
pub const fn get_quantity(&self) -> Quantity {
|
|
Quantity::from_raw(self.quantity)
|
|
}
|
|
|
|
/// Get price as Price type (None for market orders)
|
|
#[must_use]
|
|
pub const fn get_price(&self) -> Option<Price> {
|
|
if self.price == 0 {
|
|
None
|
|
} else {
|
|
Some(Price::from_raw(self.price))
|
|
}
|
|
}
|
|
|
|
/// Check if this is a buy order
|
|
#[must_use]
|
|
pub fn is_buy(&self) -> bool {
|
|
self.side == OrderSide::Buy
|
|
}
|
|
|
|
/// Check if this is a sell order
|
|
#[must_use]
|
|
pub fn is_sell(&self) -> bool {
|
|
self.side == OrderSide::Sell
|
|
}
|
|
|
|
/// Check if this is a market order
|
|
#[must_use]
|
|
pub fn is_market_order(&self) -> bool {
|
|
self.order_type == OrderType::Market || self.price == 0
|
|
}
|
|
|
|
/// Check if this is a limit order
|
|
#[must_use]
|
|
pub fn is_limit_order(&self) -> bool {
|
|
self.order_type == OrderType::Limit && self.price > 0
|
|
}
|
|
}
|
|
|
|
impl Default for OrderRef {
|
|
fn default() -> Self {
|
|
Self {
|
|
id: 0,
|
|
symbol_hash: 0,
|
|
side: OrderSide::Buy,
|
|
order_type: OrderType::Market,
|
|
quantity: 0,
|
|
price: 0,
|
|
timestamp: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// COMPREHENSIVE TESTS
|
|
// =============================================================================
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::str::FromStr;
|
|
|
|
// =============================================================================
|
|
// Price Tests
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn test_price_from_f64_valid() {
|
|
let price = Price::from_f64(100.50).unwrap();
|
|
assert_eq!(price.to_f64(), 100.50);
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_from_f64_negative() {
|
|
let result = Price::from_f64(-10.0);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_from_f64_nan() {
|
|
let result = Price::from_f64(f64::NAN);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_from_f64_infinity() {
|
|
let result = Price::from_f64(f64::INFINITY);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_constants() {
|
|
assert_eq!(Price::ZERO.to_f64(), 0.0);
|
|
assert_eq!(Price::ONE.to_f64(), 1.0);
|
|
assert_eq!(Price::CENT.to_f64(), 0.01);
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_addition() {
|
|
let p1 = Price::from_f64(10.0).unwrap();
|
|
let p2 = Price::from_f64(5.5).unwrap();
|
|
let result = p1 + p2;
|
|
assert!((result.to_f64() - 15.5).abs() < 0.00001);
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_subtraction() {
|
|
let p1 = Price::from_f64(10.0).unwrap();
|
|
let p2 = Price::from_f64(5.5).unwrap();
|
|
let result = p1 - p2;
|
|
assert!((result.to_f64() - 4.5).abs() < 0.00001);
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_multiplication() {
|
|
let price = Price::from_f64(10.0).unwrap();
|
|
let result = (price * 2.5).unwrap();
|
|
assert!((result.to_f64() - 25.0).abs() < 0.00001);
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_division() {
|
|
let price = Price::from_f64(10.0).unwrap();
|
|
let result = (price / 2.0).unwrap();
|
|
assert!((result.to_f64() - 5.0).abs() < 0.00001);
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_division_by_zero() {
|
|
let price = Price::from_f64(10.0).unwrap();
|
|
let result = price / 0.0;
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_from_cents() {
|
|
let price = Price::from_cents(150);
|
|
assert!((price.to_f64() - 1.50).abs() < 0.00001);
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_to_cents() {
|
|
let price = Price::from_f64(1.50).unwrap();
|
|
assert_eq!(price.to_cents(), 150);
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_is_zero() {
|
|
assert!(Price::ZERO.is_zero());
|
|
assert!(!Price::from_f64(1.0).unwrap().is_zero());
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_from_str() {
|
|
let price = Price::from_str("123.45").unwrap();
|
|
assert!((price.to_f64() - 123.45).abs() < 0.00001);
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_from_str_invalid() {
|
|
let result = Price::from_str("invalid");
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_display() {
|
|
let price = Price::from_f64(123.456789).unwrap();
|
|
let display = format!("{}", price);
|
|
assert!(display.starts_with("123.45678"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_partial_eq_f64() {
|
|
let price = Price::from_f64(10.0).unwrap();
|
|
assert_eq!(price, 10.0);
|
|
assert_eq!(10.0, price);
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_multiply_price() {
|
|
let p1 = Price::from_f64(10.0).unwrap();
|
|
let p2 = Price::from_f64(2.5).unwrap();
|
|
let result = p1.multiply(p2).unwrap();
|
|
assert!((result.to_f64() - 25.0).abs() < 0.00001);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Quantity Tests
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn test_quantity_from_f64_valid() {
|
|
let qty = Quantity::from_f64(100.5).unwrap();
|
|
assert_eq!(qty.to_f64(), 100.5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_quantity_from_f64_negative() {
|
|
let result = Quantity::from_f64(-10.0);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_quantity_from_f64_nan() {
|
|
let result = Quantity::from_f64(f64::NAN);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_quantity_constants() {
|
|
assert_eq!(Quantity::ZERO.to_f64(), 0.0);
|
|
assert_eq!(Quantity::ONE.to_f64(), 1.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_quantity_addition() {
|
|
let q1 = Quantity::from_f64(10.0).unwrap();
|
|
let q2 = Quantity::from_f64(5.5).unwrap();
|
|
let result = q1 + q2;
|
|
assert!((result.to_f64() - 15.5).abs() < 0.00001);
|
|
}
|
|
|
|
#[test]
|
|
fn test_quantity_subtraction() {
|
|
let q1 = Quantity::from_f64(10.0).unwrap();
|
|
let q2 = Quantity::from_f64(5.5).unwrap();
|
|
let result = q1 - q2;
|
|
assert!((result.to_f64() - 4.5).abs() < 0.00001);
|
|
}
|
|
|
|
#[test]
|
|
fn test_quantity_multiplication() {
|
|
let qty = Quantity::from_f64(10.0).unwrap();
|
|
let result = (qty * 2.5).unwrap();
|
|
assert!((result.to_f64() - 25.0).abs() < 0.00001);
|
|
}
|
|
|
|
#[test]
|
|
fn test_quantity_division() {
|
|
let qty = Quantity::from_f64(10.0).unwrap();
|
|
let result = (qty / 2.0).unwrap();
|
|
assert!((result.to_f64() - 5.0).abs() < 0.00001);
|
|
}
|
|
|
|
#[test]
|
|
fn test_quantity_division_by_zero() {
|
|
let qty = Quantity::from_f64(10.0).unwrap();
|
|
let result = qty / 0.0;
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_quantity_is_zero() {
|
|
assert!(Quantity::ZERO.is_zero());
|
|
assert!(!Quantity::from_f64(1.0).unwrap().is_zero());
|
|
}
|
|
|
|
#[test]
|
|
fn test_quantity_is_positive() {
|
|
assert!(Quantity::from_f64(1.0).unwrap().is_positive());
|
|
assert!(!Quantity::ZERO.is_positive());
|
|
}
|
|
|
|
#[test]
|
|
fn test_quantity_is_negative() {
|
|
// Quantity is always non-negative
|
|
assert!(!Quantity::from_f64(1.0).unwrap().is_negative());
|
|
assert!(!Quantity::ZERO.is_negative());
|
|
}
|
|
|
|
#[test]
|
|
fn test_quantity_from_shares() {
|
|
let qty = Quantity::from_shares(100);
|
|
assert_eq!(qty.to_shares(), 100);
|
|
}
|
|
|
|
#[test]
|
|
fn test_quantity_sum() {
|
|
let quantities = vec![
|
|
Quantity::from_f64(1.0).unwrap(),
|
|
Quantity::from_f64(2.0).unwrap(),
|
|
Quantity::from_f64(3.0).unwrap(),
|
|
];
|
|
let sum: Quantity = quantities.into_iter().sum();
|
|
assert!((sum.to_f64() - 6.0).abs() < 0.00001);
|
|
}
|
|
|
|
#[test]
|
|
fn test_quantity_try_from_i32() {
|
|
let qty = Quantity::try_from(100i32).unwrap();
|
|
assert_eq!(qty.to_f64(), 100.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_quantity_try_from_string() {
|
|
let qty = Quantity::try_from("123.45").unwrap();
|
|
assert!((qty.to_f64() - 123.45).abs() < 0.00001);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Money Tests
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn test_money_new() {
|
|
let amount = Decimal::from_f64_retain(100.50).unwrap();
|
|
let money = Money::new(amount, Currency::USD);
|
|
assert_eq!(money.currency, Currency::USD);
|
|
assert_eq!(money.amount, amount);
|
|
}
|
|
|
|
#[test]
|
|
fn test_money_display() {
|
|
let amount = Decimal::from_f64_retain(100.50).unwrap();
|
|
let money = Money::new(amount, Currency::USD);
|
|
let display = format!("{}", money);
|
|
assert!(display.contains("100.5"));
|
|
assert!(display.contains("USD"));
|
|
}
|
|
|
|
// =============================================================================
|
|
// Symbol Tests
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn test_symbol_new() {
|
|
let symbol = Symbol::new("AAPL".to_owned());
|
|
assert_eq!(symbol.as_str(), "AAPL");
|
|
}
|
|
|
|
#[test]
|
|
fn test_symbol_new_validated_valid() {
|
|
let symbol = Symbol::new_validated("AAPL".to_owned()).unwrap();
|
|
assert_eq!(symbol.as_str(), "AAPL");
|
|
}
|
|
|
|
#[test]
|
|
fn test_symbol_new_validated_empty() {
|
|
let result = Symbol::new_validated("".to_owned());
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_symbol_new_validated_whitespace() {
|
|
let result = Symbol::new_validated(" ".to_owned());
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_symbol_from_str() {
|
|
let symbol = Symbol::from_str("AAPL").unwrap();
|
|
assert_eq!(symbol.as_str(), "AAPL");
|
|
}
|
|
|
|
#[test]
|
|
fn test_symbol_to_uppercase() {
|
|
let symbol = Symbol::from_str("aapl").unwrap();
|
|
assert_eq!(symbol.to_uppercase(), "AAPL");
|
|
}
|
|
|
|
#[test]
|
|
fn test_symbol_replace() {
|
|
let symbol = Symbol::from_str("AAPL.US").unwrap();
|
|
assert_eq!(symbol.replace(".US", ""), "AAPL");
|
|
}
|
|
|
|
#[test]
|
|
fn test_symbol_contains() {
|
|
let symbol = Symbol::from_str("AAPL.US").unwrap();
|
|
assert!(symbol.contains("AAPL"));
|
|
assert!(!symbol.contains("MSFT"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_symbol_partial_eq_str() {
|
|
let symbol = Symbol::from_str("AAPL").unwrap();
|
|
assert_eq!("AAPL", symbol);
|
|
assert_eq!(symbol.as_str(), "AAPL");
|
|
}
|
|
|
|
#[test]
|
|
fn test_symbol_none() {
|
|
let symbol = Symbol::none();
|
|
assert_eq!(symbol.as_str(), "NONE");
|
|
}
|
|
|
|
// =============================================================================
|
|
// TimeInForce Tests
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn test_time_in_force_display() {
|
|
assert_eq!(format!("{}", TimeInForce::Day), "DAY");
|
|
assert_eq!(format!("{}", TimeInForce::GoodTillCancel), "GTC");
|
|
assert_eq!(format!("{}", TimeInForce::ImmediateOrCancel), "IOC");
|
|
assert_eq!(format!("{}", TimeInForce::FillOrKill), "FOK");
|
|
}
|
|
|
|
#[test]
|
|
fn test_time_in_force_default() {
|
|
assert_eq!(TimeInForce::default(), TimeInForce::Day);
|
|
}
|
|
|
|
// =============================================================================
|
|
// OrderType Tests
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn test_order_type_display() {
|
|
assert_eq!(format!("{}", OrderType::Market), "MARKET");
|
|
assert_eq!(format!("{}", OrderType::Limit), "LIMIT");
|
|
assert_eq!(format!("{}", OrderType::Stop), "STOP");
|
|
assert_eq!(format!("{}", OrderType::StopLimit), "STOP_LIMIT");
|
|
}
|
|
|
|
#[test]
|
|
fn test_order_type_try_from_i32_valid() {
|
|
assert_eq!(OrderType::try_from(0).unwrap(), OrderType::Market);
|
|
assert_eq!(OrderType::try_from(1).unwrap(), OrderType::Limit);
|
|
assert_eq!(OrderType::try_from(2).unwrap(), OrderType::Stop);
|
|
}
|
|
|
|
#[test]
|
|
fn test_order_type_try_from_i32_invalid() {
|
|
let result = OrderType::try_from(99);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_order_type_default() {
|
|
assert_eq!(OrderType::default(), OrderType::Market);
|
|
}
|
|
|
|
// =============================================================================
|
|
// OrderStatus Tests
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn test_order_status_display() {
|
|
assert_eq!(format!("{}", OrderStatus::Created), "CREATED");
|
|
assert_eq!(format!("{}", OrderStatus::Filled), "FILLED");
|
|
assert_eq!(format!("{}", OrderStatus::Cancelled), "CANCELLED");
|
|
}
|
|
|
|
#[test]
|
|
fn test_order_status_try_from_i32_valid() {
|
|
assert_eq!(OrderStatus::try_from(0).unwrap(), OrderStatus::Created);
|
|
assert_eq!(OrderStatus::try_from(3).unwrap(), OrderStatus::Filled);
|
|
assert_eq!(OrderStatus::try_from(5).unwrap(), OrderStatus::Cancelled);
|
|
}
|
|
|
|
#[test]
|
|
fn test_order_status_try_from_i32_invalid() {
|
|
let result = OrderStatus::try_from(99);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
// =============================================================================
|
|
// OrderSide Tests
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn test_order_side_display() {
|
|
assert_eq!(format!("{}", OrderSide::Buy), "BUY");
|
|
assert_eq!(format!("{}", OrderSide::Sell), "SELL");
|
|
}
|
|
|
|
#[test]
|
|
fn test_order_side_try_from_i32_valid() {
|
|
assert_eq!(OrderSide::try_from(0).unwrap(), OrderSide::Buy);
|
|
assert_eq!(OrderSide::try_from(1).unwrap(), OrderSide::Sell);
|
|
}
|
|
|
|
#[test]
|
|
fn test_order_side_try_from_i32_invalid() {
|
|
let result = OrderSide::try_from(99);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_order_side_default() {
|
|
assert_eq!(OrderSide::default(), OrderSide::Buy);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Currency Tests
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn test_currency_display() {
|
|
assert_eq!(format!("{}", Currency::USD), "USD");
|
|
assert_eq!(format!("{}", Currency::EUR), "EUR");
|
|
assert_eq!(format!("{}", Currency::BTC), "BTC");
|
|
}
|
|
|
|
#[test]
|
|
fn test_currency_default() {
|
|
assert_eq!(Currency::default(), Currency::USD);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Error Type Tests
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn test_common_type_error_invalid_price() {
|
|
let error = CommonTypeError::InvalidPrice {
|
|
value: "abc".to_owned(),
|
|
reason: "not a number".to_owned(),
|
|
};
|
|
let display = format!("{}", error);
|
|
assert!(display.contains("abc"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_common_type_error_invalid_quantity() {
|
|
let error = CommonTypeError::InvalidQuantity {
|
|
value: "xyz".to_owned(),
|
|
reason: "not a number".to_owned(),
|
|
};
|
|
let display = format!("{}", error);
|
|
assert!(display.contains("xyz"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_common_type_error_validation() {
|
|
let error = CommonTypeError::ValidationError {
|
|
field: "symbol".to_owned(),
|
|
reason: "cannot be empty".to_owned(),
|
|
};
|
|
let display = format!("{}", error);
|
|
assert!(display.contains("symbol"));
|
|
}
|
|
}
|