Files
foxhunt/market-data/src/models.rs
jgrusewski 030a15ee05 🔧 Emergency Fix: Resolve catastrophic _i32 suffix corruption (463→0 errors)
- 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
2025-10-10 23:05:26 +02:00

495 lines
14 KiB
Rust

use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
/// Price data for a financial instrument
///
/// Represents tick-level price data including bid/ask spreads,
/// last traded price, and basic OHLCV information.
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, PartialEq)]
pub struct PriceRecord {
/// Unique identifier for this price record
pub id: Uuid,
/// Trading symbol (e.g., "AAPL", "BTC/USD")
pub symbol: String,
/// Timestamp when this price was recorded
pub timestamp: DateTime<Utc>,
/// Best bid price
pub bid: Option<Decimal>,
/// Best ask price
pub ask: Option<Decimal>,
/// Last traded price
pub last: Option<Decimal>,
/// Trading volume
pub volume: Option<Decimal>,
/// Opening price for the period
pub open: Option<Decimal>,
/// Highest price for the period
pub high: Option<Decimal>,
/// Lowest price for the period
pub low: Option<Decimal>,
/// Closing price for the period
pub close: Option<Decimal>,
/// Timestamp when this record was created in the database
pub created_at: DateTime<Utc>,
}
impl PriceRecord {
/// Create a new price record with the given symbol and timestamp
///
/// # Arguments
///
/// * `symbol` - Trading symbol for the instrument
/// * `timestamp` - When this price was recorded
///
/// # Returns
///
/// A new `PriceRecord` with all price fields set to `None`
pub fn new(symbol: String, timestamp: DateTime<Utc>) -> Self {
Self {
id: Uuid::new_v4(),
symbol,
timestamp,
bid: None,
ask: None,
last: None,
volume: None,
open: None,
high: None,
low: None,
close: None,
created_at: Utc::now(),
}
}
/// Calculate mid price from bid and ask
///
/// Returns the average of bid and ask prices if both are available.
///
/// # Returns
///
/// `Some(mid_price)` if both bid and ask are available, `None` otherwise
pub fn mid_price(&self) -> Option<Decimal> {
match (self.bid, self.ask) {
(Some(bid), Some(ask)) => Some((bid + ask) / Decimal::from(2)),
_ => None,
}
}
/// Calculate spread from bid and ask
///
/// Returns the difference between ask and bid prices.
///
/// # Returns
///
/// `Some(spread)` if both bid and ask are available, `None` otherwise
pub fn spread(&self) -> Option<Decimal> {
match (self.bid, self.ask) {
(Some(bid), Some(ask)) => Some(ask - bid),
_ => None,
}
}
}
/// Order book side enumeration
///
/// Represents which side of the order book a level belongs to.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq, Hash)]
#[sqlx(type_name = "order_side", rename_all = "lowercase")]
pub enum BookSide {
/// Bid side (buy orders)
#[sqlx(rename = "bid")]
Bid,
/// Ask side (sell orders)
#[sqlx(rename = "ask")]
Ask,
}
/// Order book level data for database persistence
///
/// Represents a single level in the order book depth at a specific price point.
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, PartialEq)]
pub struct OrderBookLevelDb {
/// Unique identifier for this order book level
pub id: Uuid,
/// Trading symbol
pub symbol: String,
/// Timestamp when this level was recorded
pub timestamp: DateTime<Utc>,
/// Which side of the book (bid or ask)
pub side: BookSide,
/// Price level
pub price: Decimal,
/// Total quantity available at this price level
pub quantity: Decimal,
/// Level depth (0 = best, 1 = second best, etc.)
pub level: i32,
/// Timestamp when this record was created in the database
pub created_at: DateTime<Utc>,
}
impl OrderBookLevelDb {
/// Create a new order book level
///
/// # Arguments
///
/// * `symbol` - Trading symbol
/// * `timestamp` - When this level was recorded
///
/// * `side` - Which side of the book (bid or ask)
/// * `price` - Price level
///
/// * `quantity` - Quantity available at this price
/// * `level` - Depth level (0 = best)
///
/// # Returns
///
/// A new `OrderBookLevelDb` instance
pub fn new(
symbol: String,
timestamp: DateTime<Utc>,
side: BookSide,
price: Decimal,
quantity: Decimal,
level: i32,
) -> Self {
Self {
id: Uuid::new_v4(),
symbol,
timestamp,
side,
price,
quantity,
level,
created_at: Utc::now(),
}
}
}
/// Complete order book snapshot
///
/// Represents a full order book with all bid and ask levels at a specific point in time.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OrderBook {
/// Trading symbol
pub symbol: String,
/// Timestamp of this order book snapshot
pub timestamp: DateTime<Utc>,
/// All bid levels (buy orders) sorted by price descending
pub bids: Vec<OrderBookLevelDb>,
/// All ask levels (sell orders) sorted by price ascending
pub asks: Vec<OrderBookLevelDb>,
}
impl OrderBook {
pub fn new(symbol: String, timestamp: DateTime<Utc>) -> Self {
Self {
symbol,
timestamp,
bids: Vec::new(),
asks: Vec::new(),
}
}
/// Get the best bid price
///
/// Returns the highest bid price (best buy price) from the order book.
///
/// # Returns
///
/// `Some(price)` if there are any bids, `None` if the bid side is empty
pub fn best_bid(&self) -> Option<Decimal> {
self.bids.first().map(|level| level.price)
}
/// Get the best ask price
///
/// Returns the lowest ask price (best sell price) from the order book.
///
/// # Returns
///
/// `Some(price)` if there are any asks, `None` if the ask side is empty
pub fn best_ask(&self) -> Option<Decimal> {
self.asks.first().map(|level| level.price)
}
/// Calculate mid price from best bid and ask
///
/// Returns the average of the best bid and best ask prices.
///
/// # Returns
///
/// `Some(mid_price)` if both best bid and ask are available, `None` otherwise
pub fn mid_price(&self) -> Option<Decimal> {
match (self.best_bid(), self.best_ask()) {
(Some(bid), Some(ask)) => Some((bid + ask) / Decimal::from(2)),
_ => None,
}
}
/// Calculate spread between best bid and ask
///
/// Returns the difference between the best ask and best bid prices.
///
/// # Returns
///
/// `Some(spread)` if both best bid and ask are available, `None` otherwise
pub fn spread(&self) -> Option<Decimal> {
match (self.best_bid(), self.best_ask()) {
(Some(bid), Some(ask)) => Some(ask - bid),
_ => None,
}
}
}
/// Technical indicator types
///
/// Enumeration of supported technical analysis indicators.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq, Hash)]
#[sqlx(type_name = "indicator_type", rename_all = "lowercase")]
pub enum IndicatorType {
/// Simple Moving Average
#[sqlx(rename = "sma")]
Sma,
/// Exponential Moving Average
#[sqlx(rename = "ema")]
Ema,
/// Relative Strength Index
#[sqlx(rename = "rsi")]
Rsi,
/// Moving Average Convergence Divergence
#[sqlx(rename = "macd")]
Macd,
/// Bollinger Bands
#[sqlx(rename = "bollinger_bands")]
BollingerBands,
/// Stochastic Oscillator
#[sqlx(rename = "stochastic")]
Stochastic,
/// Average True Range
#[sqlx(rename = "atr")]
Atr,
/// Volume Weighted Average Price
#[sqlx(rename = "volume_weighted_average_price")]
VolumeWeightedAveragePrice,
}
/// Technical indicator data
///
/// Represents a computed technical indicator value at a specific point in time.
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, PartialEq)]
pub struct TechnicalIndicator {
/// Unique identifier for this indicator record
pub id: Uuid,
/// Trading symbol this indicator applies to
pub symbol: String,
/// Type of technical indicator
pub indicator_type: IndicatorType,
/// Timestamp when this indicator value was computed
pub timestamp: DateTime<Utc>,
/// The computed indicator value
pub value: Decimal,
/// Parameters used for calculation (e.g., period, smoothing factor)
pub parameters: serde_json::Value,
/// Timestamp when this record was created in the database
pub created_at: DateTime<Utc>,
}
impl TechnicalIndicator {
/// Create a new technical indicator record
///
/// # Arguments
///
/// * `symbol` - Trading symbol
/// * `indicator_type` - Type of indicator
///
/// * `timestamp` - When this indicator was computed
/// * `value` - The computed indicator value
///
/// * `parameters` - Parameters used for calculation
///
/// # Returns
///
/// A new `TechnicalIndicator` instance
pub fn new(
symbol: String,
indicator_type: IndicatorType,
timestamp: DateTime<Utc>,
value: Decimal,
parameters: serde_json::Value,
) -> Self {
Self {
id: Uuid::new_v4(),
symbol,
indicator_type,
timestamp,
value,
parameters,
created_at: Utc::now(),
}
}
}
/// Time series aggregation periods
///
/// Standard time periods used for aggregating market data into candles.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum TimePeriod {
/// 1 second interval
Second,
/// 1 minute interval
Minute,
/// 5 minute interval
FiveMinutes,
/// 15 minute interval
FifteenMinutes,
/// 30 minute interval
ThirtyMinutes,
/// 1 hour interval
Hour,
/// 4 hour interval
FourHours,
/// 1 day interval
Daily,
/// 1 week interval
Weekly,
/// 1 month interval
Monthly,
}
impl TimePeriod {
/// Get the duration in seconds for this time period
///
/// # Returns
///
/// The number of seconds in this time period
pub fn duration_seconds(&self) -> i64 {
match self {
TimePeriod::Second => 1_i64,
TimePeriod::Minute => 60_i64,
TimePeriod::FiveMinutes => 300_i64,
TimePeriod::FifteenMinutes => 900_i64,
TimePeriod::ThirtyMinutes => 1800_i64,
TimePeriod::Hour => 3600_i64,
TimePeriod::FourHours => 14400_i64,
TimePeriod::Daily => 86400_i64,
TimePeriod::Weekly => 604800_i64,
TimePeriod::Monthly => 2592000_i64, // 30 days
}
}
}
/// OHLCV (Open, High, Low, Close, Volume) candle data
///
/// Represents aggregated price data for a specific time period.
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, PartialEq)]
pub struct Candle {
/// Unique identifier for this candle
pub id: Uuid,
/// Trading symbol
pub symbol: String,
/// Time period as string (for database compatibility)
pub period: String,
/// Timestamp for the start of this candle period
pub timestamp: DateTime<Utc>,
/// Opening price for the period
pub open: Decimal,
/// Highest price during the period
pub high: Decimal,
/// Lowest price during the period
pub low: Decimal,
/// Closing price for the period
pub close: Decimal,
/// Total volume traded during the period
pub volume: Decimal,
/// Timestamp when this record was created in the database
pub created_at: DateTime<Utc>,
}
impl Candle {
/// Create a new candle
///
/// # Arguments
///
/// * `symbol` - Trading symbol
/// * `period` - Time period for this candle
///
/// * `timestamp` - Start time for this candle period
/// * `open` - Opening price
///
/// * `high` - Highest price
/// * `low` - Lowest price
///
/// * `close` - Closing price
/// * `volume` - Total volume
///
/// # Returns
///
/// A new `Candle` instance
pub fn new(
symbol: String,
period: TimePeriod,
timestamp: DateTime<Utc>,
open: Decimal,
high: Decimal,
low: Decimal,
close: Decimal,
volume: Decimal,
) -> Self {
Self {
id: Uuid::new_v4(),
symbol,
period: format!("{:?}", period),
timestamp,
open,
high,
low,
close,
volume,
created_at: Utc::now(),
}
}
/// Calculate the range (high - low)
///
/// Returns the difference between the highest and lowest prices.
///
/// # Returns
///
/// The price range for this candle
pub fn range(&self) -> Decimal {
self.high - self.low
}
/// Calculate the body (|close - open|)
///
/// Returns the absolute difference between closing and opening prices.
///
/// # Returns
///
/// The body size of this candle
pub fn body(&self) -> Decimal {
(self.close - self.open).abs()
}
/// Check if candle is bullish (close > open)
///
/// # Returns
///
/// `true` if closing price is higher than opening price
pub fn is_bullish(&self) -> bool {
self.close > self.open
}
/// Check if candle is bearish (close < open)
///
/// # Returns
///
/// `true` if closing price is lower than opening price
pub fn is_bearish(&self) -> bool {
self.close < self.open
}
}