Add dynamic universe and subscription controls

This commit is contained in:
boris
2026-04-23 07:12:56 -07:00
parent 5265f82fef
commit 152b5c3141
12 changed files with 963 additions and 24 deletions

View File

@@ -70,6 +70,8 @@ pub struct StrategyContext<'a> {
pub data: &'a DataSet,
pub portfolio: &'a PortfolioState,
pub open_orders: &'a [OpenOrderView],
pub dynamic_universe: Option<&'a BTreeSet<String>>,
pub subscriptions: &'a BTreeSet<String>,
pub process_events: &'a [ProcessEvent],
pub active_process_event: Option<&'a ProcessEvent>,
}
@@ -157,6 +159,47 @@ impl StrategyContext<'_> {
raw_sellable_qty.saturating_sub(self.symbol_open_sell_quantity(symbol))
}
pub fn has_dynamic_universe(&self) -> bool {
self.dynamic_universe
.is_some_and(|symbols| !symbols.is_empty())
}
pub fn dynamic_universe_count(&self) -> usize {
self.dynamic_universe.map_or(0, BTreeSet::len)
}
pub fn dynamic_universe_contains(&self, symbol: &str) -> bool {
self.dynamic_universe
.is_some_and(|symbols| symbols.contains(symbol))
}
pub fn eligible_universe_on(
&self,
date: NaiveDate,
) -> Vec<crate::data::EligibleUniverseSnapshot> {
let eligible = self.data.eligible_universe_on(date);
match self.dynamic_universe {
Some(symbols) if !symbols.is_empty() => eligible
.iter()
.filter(|row| symbols.contains(&row.symbol))
.cloned()
.collect(),
_ => eligible.to_vec(),
}
}
pub fn has_subscriptions(&self) -> bool {
!self.subscriptions.is_empty()
}
pub fn subscription_count(&self) -> usize {
self.subscriptions.len()
}
pub fn is_subscribed(&self, symbol: &str) -> bool {
self.subscriptions.contains(symbol)
}
pub fn has_process_events(&self) -> bool {
!self.process_events.is_empty() || self.active_process_event.is_some()
}
@@ -381,6 +424,18 @@ pub enum OrderIntent {
CancelAll {
reason: String,
},
UpdateUniverse {
symbols: BTreeSet<String>,
reason: String,
},
Subscribe {
symbols: BTreeSet<String>,
reason: String,
},
Unsubscribe {
symbols: BTreeSet<String>,
reason: String,
},
}
#[derive(Debug, Clone)]
@@ -696,6 +751,7 @@ impl Strategy for CnSmallCapRotationStrategy {
benchmark,
reference_level: signal_level,
data: ctx.data,
dynamic_universe: ctx.dynamic_universe,
});
let before_ma_count = selected_before_ma.len();
let mut ma_rejects = Vec::new();
@@ -1576,6 +1632,13 @@ impl JqMicroCapStrategy {
if !selected_set.insert(symbol.clone()) {
continue;
}
if ctx.has_dynamic_universe() && !ctx.dynamic_universe_contains(symbol) {
selected_set.remove(symbol);
if diagnostics.len() < 14 {
diagnostics.push(format!("truth {} rejected by dynamic_universe", symbol));
}
continue;
}
if let Some(reason) = self.buy_rejection_reason(ctx, date, symbol)? {
selected_set.remove(symbol);
if diagnostics.len() < 14 {
@@ -1588,8 +1651,8 @@ impl JqMicroCapStrategy {
}
if selected.len() < self.config.stocknum {
let universe = ctx.data.eligible_universe_on(date);
let start = lower_bound_eligible(universe, band_low);
let universe = ctx.eligible_universe_on(date);
let start = lower_bound_eligible(&universe, band_low);
for candidate in universe.iter().skip(start) {
if candidate.market_cap_bn > band_high {
break;
@@ -1623,10 +1686,10 @@ impl JqMicroCapStrategy {
return Ok((selected, diagnostics));
}
let universe = ctx.data.eligible_universe_on(date);
let universe = ctx.eligible_universe_on(date);
let mut diagnostics = Vec::new();
let mut selected = Vec::new();
let start = lower_bound_eligible(universe, band_low);
let start = lower_bound_eligible(&universe, band_low);
for candidate in universe.iter().skip(start) {
if candidate.market_cap_bn > band_high {