diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index f6210b1..1cbb087 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -311,10 +311,12 @@ struct SymbolPriceSeries { closes: Vec, prev_closes: Vec, last_prices: Vec, + volumes: Vec, open_prefix: Vec, close_prefix: Vec, prev_close_prefix: Vec, last_prefix: Vec, + volume_prefix: Vec, } impl SymbolPriceSeries { @@ -327,10 +329,12 @@ impl SymbolPriceSeries { let closes = sorted.iter().map(|row| row.close).collect::>(); let prev_closes = sorted.iter().map(|row| row.prev_close).collect::>(); let last_prices = sorted.iter().map(|row| row.last_price).collect::>(); + let volumes = sorted.iter().map(|row| row.volume as f64).collect::>(); let open_prefix = prefix_sums(&opens); let close_prefix = prefix_sums(&closes); let prev_close_prefix = prefix_sums(&prev_closes); let last_prefix = prefix_sums(&last_prices); + let volume_prefix = prefix_sums(&volumes); Self { dates, @@ -338,10 +342,12 @@ impl SymbolPriceSeries { closes, prev_closes, last_prices, + volumes, open_prefix, close_prefix, prev_close_prefix, last_prefix, + volume_prefix, } } @@ -396,6 +402,19 @@ impl SymbolPriceSeries { Some(sum / lookback as f64) } + fn decision_volume_moving_average(&self, date: NaiveDate, lookback: usize) -> Option { + if lookback == 0 { + return None; + } + let end = self.decision_end_index(date)?; + if end < lookback { + return None; + } + let start = end - lookback; + let sum = self.volume_prefix[end] - self.volume_prefix[start]; + Some(sum / lookback as f64) + } + fn end_index(&self, date: NaiveDate) -> Option { match self.dates.binary_search(&date) { Ok(idx) => Some(idx + 1), @@ -797,6 +816,17 @@ impl DataSet { .and_then(|series| series.decision_close_moving_average(date, lookback)) } + pub fn market_decision_volume_moving_average( + &self, + date: NaiveDate, + symbol: &str, + lookback: usize, + ) -> Option { + self.market_series_by_symbol + .get(symbol) + .and_then(|series| series.decision_volume_moving_average(date, lookback)) + } + pub fn market_moving_average( &self, date: NaiveDate, diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 0c92a90..4f6d23a 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -173,6 +173,10 @@ struct StockExpressionState { stock_ma10: f64, stock_ma20: f64, stock_ma30: f64, + stock_volume_ma5: f64, + stock_volume_ma10: f64, + stock_volume_ma20: f64, + stock_volume_ma60: f64, extra_factors: BTreeMap, } @@ -772,6 +776,22 @@ impl PlatformExprStrategy { .data .market_decision_close_moving_average(date, symbol, 30) .unwrap_or(stock_ma20); + let stock_volume_ma5 = ctx + .data + .market_decision_volume_moving_average(date, symbol, 5) + .unwrap_or(market.volume as f64); + let stock_volume_ma10 = ctx + .data + .market_decision_volume_moving_average(date, symbol, 10) + .unwrap_or(stock_volume_ma5); + let stock_volume_ma20 = ctx + .data + .market_decision_volume_moving_average(date, symbol, 20) + .unwrap_or(stock_volume_ma10); + let stock_volume_ma60 = ctx + .data + .market_decision_volume_moving_average(date, symbol, 60) + .unwrap_or(stock_volume_ma20); Ok(StockExpressionState { symbol: symbol.to_string(), @@ -807,6 +827,10 @@ impl PlatformExprStrategy { stock_ma10, stock_ma20, stock_ma30, + stock_volume_ma5, + stock_volume_ma10, + stock_volume_ma20, + stock_volume_ma60, extra_factors: factor.extra_factors.clone(), }) } @@ -920,6 +944,14 @@ impl PlatformExprStrategy { scope.push("ma10", stock.stock_ma10); scope.push("ma20", stock.stock_ma20); scope.push("ma30", stock.stock_ma30); + scope.push("stock_volume_ma5", stock.stock_volume_ma5); + scope.push("stock_volume_ma10", stock.stock_volume_ma10); + scope.push("stock_volume_ma20", stock.stock_volume_ma20); + scope.push("stock_volume_ma60", stock.stock_volume_ma60); + scope.push("volume_ma5", stock.stock_volume_ma5); + scope.push("volume_ma10", stock.stock_volume_ma10); + scope.push("volume_ma20", stock.stock_volume_ma20); + scope.push("volume_ma60", stock.stock_volume_ma60); let mut factors = Map::new(); factors.insert("symbol".into(), Dynamic::from(stock.symbol.clone())); factors.insert("market_cap".into(), Dynamic::from(stock.market_cap)); @@ -960,6 +992,14 @@ impl PlatformExprStrategy { factors.insert("ma10".into(), Dynamic::from(stock.stock_ma10)); factors.insert("ma20".into(), Dynamic::from(stock.stock_ma20)); factors.insert("ma30".into(), Dynamic::from(stock.stock_ma30)); + factors.insert("stock_volume_ma5".into(), Dynamic::from(stock.stock_volume_ma5)); + factors.insert("stock_volume_ma10".into(), Dynamic::from(stock.stock_volume_ma10)); + factors.insert("stock_volume_ma20".into(), Dynamic::from(stock.stock_volume_ma20)); + factors.insert("stock_volume_ma60".into(), Dynamic::from(stock.stock_volume_ma60)); + factors.insert("volume_ma5".into(), Dynamic::from(stock.stock_volume_ma5)); + factors.insert("volume_ma10".into(), Dynamic::from(stock.stock_volume_ma10)); + factors.insert("volume_ma20".into(), Dynamic::from(stock.stock_volume_ma20)); + factors.insert("volume_ma60".into(), Dynamic::from(stock.stock_volume_ma60)); for (key, value) in &stock.extra_factors { factors.insert(key.clone().into(), Dynamic::from(*value)); } @@ -1288,6 +1328,10 @@ impl PlatformExprStrategy { "ma5" => Some(stock.stock_ma5), "ma10" => Some(stock.stock_ma10), "ma20" => Some(stock.stock_ma20), + "stock_volume_ma5" | "volume_ma5" => Some(stock.stock_volume_ma5), + "stock_volume_ma10" | "volume_ma10" => Some(stock.stock_volume_ma10), + "stock_volume_ma20" | "volume_ma20" => Some(stock.stock_volume_ma20), + "stock_volume_ma60" | "volume_ma60" => Some(stock.stock_volume_ma60), "allow_buy" => Some(if stock.allow_buy { 1.0 } else { 0.0 }), "allow_sell" => Some(if stock.allow_sell { 1.0 } else { 0.0 }), "paused" => Some(if stock.paused { 1.0 } else { 0.0 }),