Expose strategy runtime data APIs
This commit is contained in:
@@ -144,6 +144,11 @@ struct TickProbeStrategy {
|
||||
ordered: bool,
|
||||
}
|
||||
|
||||
struct DataApiProbeStrategy {
|
||||
target_date: NaiveDate,
|
||||
snapshots: Rc<RefCell<Vec<String>>>,
|
||||
}
|
||||
|
||||
impl Strategy for ScheduledProbeStrategy {
|
||||
fn name(&self) -> &str {
|
||||
"scheduled-probe"
|
||||
@@ -325,8 +330,20 @@ impl Strategy for TickProbeStrategy {
|
||||
ctx: &StrategyContext<'_>,
|
||||
quote: &IntradayExecutionQuote,
|
||||
) -> Result<StrategyDecision, fidc_core::BacktestError> {
|
||||
let visible_last = ctx
|
||||
.history_bars("e.symbol, 9, "tick", "last", true)
|
||||
.iter()
|
||||
.map(|value| format!("{value:.2}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let previous_last = ctx
|
||||
.history_bars("e.symbol, 9, "tick", "last", false)
|
||||
.iter()
|
||||
.map(|value| format!("{value:.2}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
self.seen_ticks.borrow_mut().push(format!(
|
||||
"{}:{}:{}",
|
||||
"{}:{}:{}:visible={visible_last}:previous={previous_last}",
|
||||
quote.symbol,
|
||||
quote.timestamp.time(),
|
||||
ctx.is_subscribed("e.symbol)
|
||||
@@ -350,6 +367,68 @@ impl Strategy for TickProbeStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
impl Strategy for DataApiProbeStrategy {
|
||||
fn name(&self) -> &str {
|
||||
"data-api-probe"
|
||||
}
|
||||
|
||||
fn on_day(
|
||||
&mut self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
) -> Result<StrategyDecision, fidc_core::BacktestError> {
|
||||
if ctx.execution_date == self.target_date {
|
||||
let daily_close = ctx
|
||||
.history_bars("000001.SZ", 2, "1d", "close", true)
|
||||
.iter()
|
||||
.map(|value| format!("{value:.2}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let previous_close = ctx
|
||||
.history_bars("000001.SZ", 2, "daily", "close", false)
|
||||
.iter()
|
||||
.map(|value| format!("{value:.2}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let tick_last = ctx
|
||||
.history_bars("000001.SZ", 2, "1m", "last", true)
|
||||
.iter()
|
||||
.map(|value| format!("{value:.2}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let previous_tick_last = ctx
|
||||
.history_bars("000001.SZ", 2, "1m", "last", false)
|
||||
.iter()
|
||||
.map(|value| format!("{value:.2}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let current_close = ctx
|
||||
.current_snapshot("000001.SZ")
|
||||
.map(|snapshot| format!("{:.2}", snapshot.close))
|
||||
.unwrap_or_default();
|
||||
let instrument_name = ctx
|
||||
.instrument("000001.SZ")
|
||||
.map(|instrument| instrument.name.clone())
|
||||
.unwrap_or_default();
|
||||
let prev_date = ctx
|
||||
.get_previous_trading_date(ctx.execution_date, 1)
|
||||
.map(|date| date.to_string())
|
||||
.unwrap_or_default();
|
||||
let next_date = ctx
|
||||
.get_next_trading_date(d(2025, 1, 3), 1)
|
||||
.map(|date| date.to_string())
|
||||
.unwrap_or_default();
|
||||
let trading_date_count = ctx
|
||||
.get_trading_dates(d(2025, 1, 2), ctx.execution_date)
|
||||
.len();
|
||||
self.snapshots.borrow_mut().push(format!(
|
||||
"daily={daily_close};previous={previous_close};tick={tick_last};previous_tick={previous_tick_last};current={current_close};instrument={instrument_name};all={};range={trading_date_count};prev={prev_date};next={next_date}",
|
||||
ctx.all_instruments().len()
|
||||
));
|
||||
}
|
||||
Ok(StrategyDecision::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_runs_strategy_hooks_in_daily_order() {
|
||||
let date1 = d(2025, 1, 2);
|
||||
@@ -769,7 +848,10 @@ fn engine_runs_subscribed_tick_hooks_and_executes_tick_orders() {
|
||||
|
||||
assert_eq!(
|
||||
seen_ticks.borrow().as_slice(),
|
||||
["000001.SZ:10:18:00:true", "000001.SZ:10:19:00:true"]
|
||||
[
|
||||
"000001.SZ:10:18:00:true:visible=10.20:previous=",
|
||||
"000001.SZ:10:19:00:true:visible=10.20,10.30:previous=10.20"
|
||||
]
|
||||
);
|
||||
assert_eq!(result.fills.len(), 1);
|
||||
assert_eq!(result.fills[0].reason, "tick_buy");
|
||||
@@ -794,6 +876,180 @@ fn engine_runs_subscribed_tick_hooks_and_executes_tick_orders() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strategy_context_exposes_rqalpha_style_data_helpers() {
|
||||
let date1 = d(2025, 1, 2);
|
||||
let date2 = d(2025, 1, 3);
|
||||
let date3 = d(2025, 1, 6);
|
||||
let instrument = Instrument {
|
||||
symbol: "000001.SZ".to_string(),
|
||||
name: "Anchor".to_string(),
|
||||
board: "SZ".to_string(),
|
||||
round_lot: 100,
|
||||
listed_at: Some(d(2020, 1, 1)),
|
||||
delisted_at: None,
|
||||
status: "active".to_string(),
|
||||
};
|
||||
let market = [
|
||||
(date1, 10.0, 10.0, 10.0, 100_000),
|
||||
(date2, 10.1, 10.1, 10.0, 110_000),
|
||||
(date3, 10.2, 10.2, 10.1, 120_000),
|
||||
]
|
||||
.into_iter()
|
||||
.map(
|
||||
|(date, open, close, prev_close, volume)| DailyMarketSnapshot {
|
||||
date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
timestamp: Some(format!("{date} 10:18:00")),
|
||||
day_open: open,
|
||||
open,
|
||||
high: close + 0.2,
|
||||
low: close - 0.2,
|
||||
close,
|
||||
last_price: close,
|
||||
bid1: close - 0.01,
|
||||
ask1: close + 0.01,
|
||||
prev_close,
|
||||
volume,
|
||||
tick_volume: volume,
|
||||
bid1_volume: volume,
|
||||
ask1_volume: volume,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit: prev_close * 1.1,
|
||||
lower_limit: prev_close * 0.9,
|
||||
price_tick: 0.01,
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
let factors = [date1, date2, date3]
|
||||
.into_iter()
|
||||
.map(|date| DailyFactorSnapshot {
|
||||
date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
market_cap_bn: 20.0,
|
||||
free_float_cap_bn: 18.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
extra_factors: BTreeMap::new(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let candidates = [date1, date2, date3]
|
||||
.into_iter()
|
||||
.map(|date| CandidateEligibility {
|
||||
date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let benchmarks = [
|
||||
(date1, 100.0, 99.0),
|
||||
(date2, 101.0, 100.0),
|
||||
(date3, 102.0, 101.0),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(date, close, prev_close)| BenchmarkSnapshot {
|
||||
date,
|
||||
benchmark: "000300.SH".to_string(),
|
||||
open: close,
|
||||
close,
|
||||
prev_close,
|
||||
volume: 1_000_000,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let quotes = vec![
|
||||
IntradayExecutionQuote {
|
||||
date: date2,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
timestamp: dt(2025, 1, 3, 14, 30, 0),
|
||||
last_price: 10.15,
|
||||
bid1: 10.14,
|
||||
ask1: 10.15,
|
||||
bid1_volume: 1000,
|
||||
ask1_volume: 1000,
|
||||
volume_delta: 1000,
|
||||
amount_delta: 10_150.0,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
},
|
||||
IntradayExecutionQuote {
|
||||
date: date3,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
timestamp: dt(2025, 1, 6, 10, 18, 0),
|
||||
last_price: 10.25,
|
||||
bid1: 10.24,
|
||||
ask1: 10.25,
|
||||
bid1_volume: 1000,
|
||||
ask1_volume: 1000,
|
||||
volume_delta: 1000,
|
||||
amount_delta: 10_250.0,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
},
|
||||
IntradayExecutionQuote {
|
||||
date: date3,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
timestamp: dt(2025, 1, 6, 10, 19, 0),
|
||||
last_price: 10.26,
|
||||
bid1: 10.25,
|
||||
ask1: 10.26,
|
||||
bid1_volume: 1000,
|
||||
ask1_volume: 1000,
|
||||
volume_delta: 1000,
|
||||
amount_delta: 10_260.0,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
},
|
||||
];
|
||||
let data = DataSet::from_components_with_actions_and_quotes(
|
||||
vec![instrument],
|
||||
market,
|
||||
factors,
|
||||
candidates,
|
||||
benchmarks,
|
||||
Vec::new(),
|
||||
quotes,
|
||||
)
|
||||
.expect("dataset");
|
||||
|
||||
let snapshots = Rc::new(RefCell::new(Vec::new()));
|
||||
let strategy = DataApiProbeStrategy {
|
||||
target_date: date3,
|
||||
snapshots: snapshots.clone(),
|
||||
};
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
);
|
||||
let mut engine = BacktestEngine::new(
|
||||
data,
|
||||
strategy,
|
||||
broker,
|
||||
BacktestConfig {
|
||||
initial_cash: 10_000.0,
|
||||
benchmark_code: "000300.SH".to_string(),
|
||||
start_date: Some(date1),
|
||||
end_date: Some(date3),
|
||||
decision_lag_trading_days: 0,
|
||||
execution_price_field: PriceField::Open,
|
||||
},
|
||||
);
|
||||
|
||||
engine.run().expect("backtest run");
|
||||
|
||||
assert_eq!(
|
||||
snapshots.borrow().as_slice(),
|
||||
[
|
||||
"daily=10.10,10.20;previous=10.00,10.10;tick=10.15,10.25;previous_tick=10.15;current=10.20;instrument=Anchor;all=1;range=3;prev=2025-01-03;next=2025-01-06"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_rejects_pending_limit_orders_at_market_close() {
|
||||
let date1 = d(2025, 1, 2);
|
||||
|
||||
@@ -32,6 +32,7 @@ fn strategy_emits_target_weights_and_diagnostics() {
|
||||
subscriptions: &subscriptions,
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
})
|
||||
.expect("decision");
|
||||
|
||||
@@ -75,6 +76,7 @@ fn jq_strategy_emits_same_day_decision() {
|
||||
subscriptions: &subscriptions,
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
})
|
||||
.expect("jq decision");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user