按实际委托时间选择盘后撮合阶段
This commit is contained in:
@@ -83,6 +83,8 @@
|
||||
|
||||
Source Lake 日线成交量保留原始可用性合同:源 `volume=null` 与真实 `volume=0` 含义不同。依赖成交量的 rolling 窗口只要包含源空值就返回缺失,不得把空值补成 0;停牌日明确提供的 0 成交量仍是合法观测。该合同随 runner 快照版本冻结,旧快照不能跨版本复用。
|
||||
|
||||
盘后固定价格不是策略类型,也不是 `matchingType`。自 2026-07-06 起,只有实际同日提交时间落在 15:00–15:30 的普通委托才由 broker 进入盘后固定价格执行阶段;15:00–15:04 的委托等待到 15:05,15:05–15:30 按官方收盘价和真实盘后成交量撮合,不叠加滑点,未成交余量不跨日。窗口外委托继续沿用连续竞价、当前收盘或下一交易日开盘合同;`next_bar_open` 策略即使在 15:00 生成信号,也不得被改写为同日盘后委托。缺失盘后行情时必须明确不成交,禁止回退全天成交量或 15:00 前分钟行情。
|
||||
|
||||
`holdUntilExit=true` 与 `stopTakeReferencePriceMode=signal_day_post_adjusted_close` 组合表示持久模型组合语义:股票进入模型目标后即记录信号日和后复权参考价,不以买单是否成交为前提。涨停、停牌或其他执行风控导致买单未成交时,模型成员仍占用目标槽位、每天累计模型持有日并继续生成目标仓位;达到止盈、止损或最大模型持有期后才从模型组合移除。实际订单仍由成交日风控独立决定,不得用实际持仓集合覆盖模型目标集合。
|
||||
|
||||
## 内置微盘策略
|
||||
|
||||
+243
-18
@@ -255,6 +255,12 @@ pub enum MatchingType {
|
||||
Twap,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum EquityExecutionPhase {
|
||||
ContinuousAuction,
|
||||
PostCloseFixedPrice,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RebalanceCashMode {
|
||||
SamePointNet,
|
||||
@@ -544,7 +550,81 @@ impl<C, R> BrokerSimulator<C, R> {
|
||||
}
|
||||
}
|
||||
|
||||
fn effective_remainder_policy(&self, allow_pending_limit: bool) -> RemainderPolicy {
|
||||
fn submission_time(&self) -> Option<NaiveTime> {
|
||||
self.runtime_intraday_start_time
|
||||
.get()
|
||||
.or(self.intraday_execution_start_time)
|
||||
}
|
||||
|
||||
fn execution_phase(&self, date: NaiveDate) -> EquityExecutionPhase {
|
||||
let effective_date = NaiveDate::from_ymd_opt(2026, 7, 6).expect("valid effective date");
|
||||
let window_start = NaiveTime::from_hms_opt(15, 0, 0).expect("valid window start");
|
||||
let window_end = NaiveTime::from_hms_opt(15, 30, 0).expect("valid window end");
|
||||
let submitted_same_day = self
|
||||
.runtime_order_created_date
|
||||
.get()
|
||||
.is_none_or(|created_date| created_date == date);
|
||||
|
||||
if date >= effective_date
|
||||
&& submitted_same_day
|
||||
&& !matches!(
|
||||
self.matching_type,
|
||||
MatchingType::OpenAuction | MatchingType::NextBarOpen
|
||||
)
|
||||
&& self
|
||||
.submission_time()
|
||||
.is_some_and(|time| time >= window_start && time <= window_end)
|
||||
{
|
||||
EquityExecutionPhase::PostCloseFixedPrice
|
||||
} else {
|
||||
EquityExecutionPhase::ContinuousAuction
|
||||
}
|
||||
}
|
||||
|
||||
fn is_post_close_fixed_price(&self, date: NaiveDate) -> bool {
|
||||
self.execution_phase(date) == EquityExecutionPhase::PostCloseFixedPrice
|
||||
}
|
||||
|
||||
fn effective_execution_price_field(&self, date: NaiveDate) -> PriceField {
|
||||
if self.is_post_close_fixed_price(date) {
|
||||
PriceField::Close
|
||||
} else {
|
||||
self.execution_price_field
|
||||
}
|
||||
}
|
||||
|
||||
fn post_close_execution_window(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
) -> Option<(NaiveDateTime, NaiveDateTime)> {
|
||||
self.post_close_execution_quote_window(date)
|
||||
.map(|(start, end)| (date.and_time(start), date.and_time(end)))
|
||||
}
|
||||
|
||||
pub(crate) fn post_close_execution_quote_window(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
) -> Option<(NaiveTime, NaiveTime)> {
|
||||
if !self.is_post_close_fixed_price(date) {
|
||||
return None;
|
||||
}
|
||||
let matching_start = NaiveTime::from_hms_opt(15, 5, 0).expect("valid matching start");
|
||||
let matching_end = NaiveTime::from_hms_opt(15, 30, 0).expect("valid matching end");
|
||||
let submitted_at = self.submission_time()?;
|
||||
Some((submitted_at.max(matching_start), matching_end))
|
||||
}
|
||||
|
||||
fn effective_remainder_policy(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
allow_pending_limit: bool,
|
||||
) -> RemainderPolicy {
|
||||
if self.is_post_close_fixed_price(date) {
|
||||
return match self.runtime_time_in_force.get() {
|
||||
Some(OrderTimeInForce::Fok) => RemainderPolicy::FillOrKill,
|
||||
_ => RemainderPolicy::Cancel,
|
||||
};
|
||||
}
|
||||
match self.runtime_time_in_force.get() {
|
||||
Some(OrderTimeInForce::Fok) => RemainderPolicy::FillOrKill,
|
||||
Some(OrderTimeInForce::Gtc) => RemainderPolicy::KeepUntilCanceled,
|
||||
@@ -613,15 +693,15 @@ where
|
||||
R: EquityRuleHooks,
|
||||
{
|
||||
fn buy_price(&self, snapshot: &crate::data::DailyMarketSnapshot) -> f64 {
|
||||
snapshot.buy_price(self.execution_price_field)
|
||||
snapshot.buy_price(self.effective_execution_price_field(snapshot.date))
|
||||
}
|
||||
|
||||
fn sell_price(&self, snapshot: &crate::data::DailyMarketSnapshot) -> f64 {
|
||||
snapshot.sell_price(self.execution_price_field)
|
||||
snapshot.sell_price(self.effective_execution_price_field(snapshot.date))
|
||||
}
|
||||
|
||||
fn sizing_price(&self, snapshot: &crate::data::DailyMarketSnapshot) -> f64 {
|
||||
snapshot.price(self.execution_price_field)
|
||||
snapshot.price(self.effective_execution_price_field(snapshot.date))
|
||||
}
|
||||
|
||||
fn value_buy_sizing_price(
|
||||
@@ -651,6 +731,9 @@ where
|
||||
symbol: &str,
|
||||
snapshot: &crate::data::DailyMarketSnapshot,
|
||||
) -> f64 {
|
||||
if self.is_post_close_fixed_price(date) {
|
||||
return snapshot.close;
|
||||
}
|
||||
if self.matching_type == MatchingType::NextBarOpen {
|
||||
let execution_price = snapshot.price(PriceField::Open);
|
||||
if execution_price.is_finite() && execution_price > 0.0 {
|
||||
@@ -907,6 +990,9 @@ where
|
||||
snapshot: &crate::data::DailyMarketSnapshot,
|
||||
side: OrderSide,
|
||||
) -> f64 {
|
||||
if self.is_post_close_fixed_price(date) {
|
||||
return snapshot.close;
|
||||
}
|
||||
let start_cursor = self
|
||||
.runtime_intraday_start_time
|
||||
.get()
|
||||
@@ -940,6 +1026,9 @@ where
|
||||
snapshot: &crate::data::DailyMarketSnapshot,
|
||||
side: OrderSide,
|
||||
) -> f64 {
|
||||
if self.is_post_close_fixed_price(snapshot.date) {
|
||||
return snapshot.close;
|
||||
}
|
||||
if self.execution_price_field == PriceField::Last
|
||||
&& self.intraday_execution_start_time.is_some()
|
||||
{
|
||||
@@ -956,7 +1045,7 @@ where
|
||||
snapshot: &crate::data::DailyMarketSnapshot,
|
||||
side: OrderSide,
|
||||
) -> f64 {
|
||||
let price = snapshot.price(self.execution_price_field);
|
||||
let price = snapshot.price(self.effective_execution_price_field(snapshot.date));
|
||||
if price.is_finite() && price > 0.0 {
|
||||
price
|
||||
} else {
|
||||
@@ -983,6 +1072,10 @@ where
|
||||
return self.clamp_execution_price(snapshot, side, raw_price);
|
||||
}
|
||||
|
||||
if self.is_post_close_fixed_price(snapshot.date) {
|
||||
return self.clamp_execution_price(snapshot, side, raw_price);
|
||||
}
|
||||
|
||||
let order_value = quantity.and_then(|qty| (qty > 0).then_some(raw_price * qty as f64));
|
||||
let mut adjusted = match self.slippage_model {
|
||||
SlippageModel::None => raw_price,
|
||||
@@ -1069,11 +1162,15 @@ where
|
||||
|
||||
fn select_quote_reference_price(
|
||||
&self,
|
||||
_snapshot: &crate::data::DailyMarketSnapshot,
|
||||
snapshot: &crate::data::DailyMarketSnapshot,
|
||||
quote: &IntradayExecutionQuote,
|
||||
side: OrderSide,
|
||||
matching_type: MatchingType,
|
||||
) -> Option<f64> {
|
||||
if self.is_post_close_fixed_price(snapshot.date) {
|
||||
return (snapshot.close.is_finite() && snapshot.close > 0.0)
|
||||
.then_some(snapshot.close);
|
||||
}
|
||||
let raw_price = match matching_type {
|
||||
MatchingType::MinuteBestOwn => match side {
|
||||
OrderSide::Buy => {
|
||||
@@ -4014,7 +4111,7 @@ where
|
||||
algo_request: Option<&AlgoExecutionRequest>,
|
||||
report: &mut BrokerExecutionReport,
|
||||
) -> Result<(), BacktestError> {
|
||||
let remainder_policy = self.effective_remainder_policy(allow_pending_limit);
|
||||
let remainder_policy = self.effective_remainder_policy(date, allow_pending_limit);
|
||||
let Some(position) = portfolio.position(symbol) else {
|
||||
return Ok(());
|
||||
};
|
||||
@@ -5725,7 +5822,7 @@ where
|
||||
algo_request: Option<&AlgoExecutionRequest>,
|
||||
report: &mut BrokerExecutionReport,
|
||||
) -> Result<(), BacktestError> {
|
||||
let remainder_policy = self.effective_remainder_policy(allow_pending_limit);
|
||||
let remainder_policy = self.effective_remainder_policy(date, allow_pending_limit);
|
||||
if portfolio
|
||||
.position(symbol)
|
||||
.is_none_or(|position| position.quantity == 0)
|
||||
@@ -6925,23 +7022,29 @@ where
|
||||
limit_price: Option<f64>,
|
||||
) -> Option<ExecutionFill> {
|
||||
let matching_type = self.matching_type_for_algo_request(algo_request);
|
||||
let use_intraday_quotes =
|
||||
algo_request.is_some() || self.execution_price_field == PriceField::Last;
|
||||
let post_close_window = self.post_close_execution_window(date);
|
||||
let use_intraday_quotes = post_close_window.is_some()
|
||||
|| algo_request.is_some()
|
||||
|| self.execution_price_field == PriceField::Last;
|
||||
if !use_intraday_quotes {
|
||||
return None;
|
||||
}
|
||||
|
||||
let runtime_start_time = self.runtime_intraday_start_time.get();
|
||||
let runtime_end_time = self.runtime_intraday_end_time.get();
|
||||
let start_cursor = algo_request
|
||||
let start_cursor = post_close_window.map(|window| window.0).or_else(|| {
|
||||
algo_request
|
||||
.and_then(|request| request.start_time)
|
||||
.or(runtime_start_time)
|
||||
.or(self.intraday_execution_start_time)
|
||||
.map(|start_time| date.and_time(start_time));
|
||||
let end_cursor = algo_request
|
||||
.map(|start_time| date.and_time(start_time))
|
||||
});
|
||||
let end_cursor = post_close_window.map(|window| window.1).or_else(|| {
|
||||
algo_request
|
||||
.and_then(|request| request.end_time)
|
||||
.or(runtime_end_time)
|
||||
.map(|end_time| date.and_time(end_time));
|
||||
.map(|end_time| date.and_time(end_time))
|
||||
});
|
||||
let quotes = data.execution_quotes_on(date, symbol);
|
||||
|
||||
if let Some(fill) = self.select_execution_fill_with_ledger(
|
||||
@@ -6965,7 +7068,8 @@ where
|
||||
return Some(fill);
|
||||
}
|
||||
|
||||
if algo_request.is_some()
|
||||
if post_close_window.is_some()
|
||||
|| algo_request.is_some()
|
||||
|| runtime_start_time.is_some()
|
||||
|| runtime_end_time.is_some()
|
||||
|| self.intraday_execution_start_time.is_some()
|
||||
@@ -7088,7 +7192,8 @@ where
|
||||
&& start_cursor.is_some()
|
||||
&& end_cursor.is_some()
|
||||
&& start_cursor == end_cursor;
|
||||
let use_decision_time_quote = start_cursor.is_some()
|
||||
let use_decision_time_quote = !self.is_post_close_fixed_price(snapshot.date)
|
||||
&& start_cursor.is_some()
|
||||
&& (matching_type == MatchingType::MinuteLast || exact_time_order_quote);
|
||||
let eligible_quotes: Vec<&IntradayExecutionQuote> = if use_decision_time_quote {
|
||||
self.latest_known_quote_at_or_before(
|
||||
@@ -7531,9 +7636,11 @@ fn sell_reason(decision: &StrategyDecision, symbol: &str) -> &'static str {
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use chrono::NaiveTime;
|
||||
|
||||
use super::{
|
||||
BrokerExecutionReport, BrokerSimulator, IntradayExecutionLedger, MatchingType, OpenOrder,
|
||||
RebalanceCashMode, SlippageModel,
|
||||
BrokerExecutionReport, BrokerSimulator, EquityExecutionPhase, IntradayExecutionLedger,
|
||||
MatchingType, OpenOrder, RebalanceCashMode, SlippageModel,
|
||||
};
|
||||
use crate::cost::ChinaAShareCostModel;
|
||||
use crate::data::{
|
||||
@@ -7769,6 +7876,124 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_close_phase_is_derived_from_actual_same_day_submission_time() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2026, 7, 6).expect("valid date");
|
||||
let before_effective = chrono::NaiveDate::from_ymd_opt(2026, 7, 3).expect("valid date");
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_matching_type(MatchingType::CurrentBarClose)
|
||||
.with_slippage_model(SlippageModel::PriceRatio(0.25));
|
||||
broker.runtime_order_created_date.set(Some(date));
|
||||
let mut snapshot = dated_limit_test_snapshot(date);
|
||||
snapshot.close = 10.0;
|
||||
snapshot.upper_limit = 20.0;
|
||||
|
||||
for (hour, minute) in [(14, 59), (15, 31)] {
|
||||
broker
|
||||
.runtime_intraday_start_time
|
||||
.set(NaiveTime::from_hms_opt(hour, minute, 0));
|
||||
assert_eq!(
|
||||
broker.execution_phase(date),
|
||||
EquityExecutionPhase::ContinuousAuction
|
||||
);
|
||||
assert_eq!(
|
||||
broker.snapshot_execution_price(&snapshot, OrderSide::Buy, Some(100)),
|
||||
12.5
|
||||
);
|
||||
}
|
||||
for (hour, minute) in [(15, 0), (15, 5), (15, 30)] {
|
||||
broker
|
||||
.runtime_intraday_start_time
|
||||
.set(NaiveTime::from_hms_opt(hour, minute, 0));
|
||||
assert_eq!(
|
||||
broker.execution_phase(date),
|
||||
EquityExecutionPhase::PostCloseFixedPrice
|
||||
);
|
||||
assert_eq!(
|
||||
broker.snapshot_execution_price(&snapshot, OrderSide::Buy, Some(100)),
|
||||
10.0
|
||||
);
|
||||
}
|
||||
broker
|
||||
.runtime_order_created_date
|
||||
.set(Some(before_effective));
|
||||
assert_eq!(
|
||||
broker.execution_phase(before_effective),
|
||||
EquityExecutionPhase::ContinuousAuction
|
||||
);
|
||||
|
||||
let next_open = BrokerSimulator::new(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks,
|
||||
)
|
||||
.with_matching_type(MatchingType::NextBarOpen)
|
||||
.with_intraday_execution_start_time(
|
||||
NaiveTime::from_hms_opt(15, 0, 0).expect("valid signal time"),
|
||||
);
|
||||
next_open
|
||||
.runtime_order_created_date
|
||||
.set(Some(date.pred_opt().expect("previous date")));
|
||||
assert_eq!(
|
||||
next_open.execution_phase(date),
|
||||
EquityExecutionPhase::ContinuousAuction,
|
||||
"a 15:00 signal for next-open execution is not a same-day post-close order"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_close_order_uses_close_without_slippage_and_waits_until_matching_window() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2026, 7, 6).expect("valid date");
|
||||
let mut snapshot = dated_limit_test_snapshot(date);
|
||||
snapshot.close = 10.0;
|
||||
snapshot.last_price = 12.0;
|
||||
snapshot.bid1 = 11.99;
|
||||
snapshot.ask1 = 12.01;
|
||||
snapshot.upper_limit = 20.0;
|
||||
snapshot.lower_limit = 1.0;
|
||||
let mut quote_before_matching = limit_test_quote(12.0, 11.99, 12.01);
|
||||
quote_before_matching.date = date;
|
||||
quote_before_matching.timestamp = date.and_hms_opt(15, 4, 0).expect("valid timestamp");
|
||||
quote_before_matching.volume_delta = 100_000;
|
||||
let mut quote_at_matching = quote_before_matching.clone();
|
||||
quote_at_matching.timestamp = date.and_hms_opt(15, 5, 0).expect("valid timestamp");
|
||||
let data = DataSet::from_components_with_actions_and_quotes(
|
||||
vec![limit_test_instrument()],
|
||||
vec![snapshot],
|
||||
Vec::new(),
|
||||
vec![dated_limit_test_candidate(date, false, false, true, true)],
|
||||
vec![dated_limit_test_benchmark(date)],
|
||||
Vec::new(),
|
||||
vec![quote_before_matching, quote_at_matching],
|
||||
)
|
||||
.expect("valid post-close dataset");
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_matching_type(MatchingType::CurrentBarClose)
|
||||
.with_slippage_model(SlippageModel::PriceRatio(0.25))
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(false);
|
||||
let mut portfolio = PortfolioState::new(100_000.0);
|
||||
let report = broker
|
||||
.execute_between(
|
||||
date,
|
||||
&mut portfolio,
|
||||
&data,
|
||||
&next_open_buy_decision(),
|
||||
NaiveTime::from_hms_opt(15, 0, 0),
|
||||
NaiveTime::from_hms_opt(15, 0, 0),
|
||||
)
|
||||
.expect("post-close order executes");
|
||||
|
||||
assert_eq!(report.fill_events.len(), 1, "{report:?}");
|
||||
let fill = &report.fill_events[0];
|
||||
assert_eq!(fill.price, 10.0, "fixed-price trading uses official close");
|
||||
assert_eq!(
|
||||
fill.execution_timestamp,
|
||||
date.and_hms_opt(15, 5, 0),
|
||||
"15:00 submission must not consume the 15:04 quote"
|
||||
);
|
||||
assert!(!broker.has_open_orders());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minute_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity() {
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
|
||||
@@ -638,15 +638,23 @@ where
|
||||
if self.execution_quote_loader.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let post_close_window = self
|
||||
.broker
|
||||
.post_close_execution_quote_window(execution_date);
|
||||
if self.broker.execution_price_field() != PriceField::Last
|
||||
&& !decision_has_algo_execution(decision)
|
||||
&& post_close_window.is_none()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let caller_start_time = start_time;
|
||||
let caller_end_time = end_time;
|
||||
let start_time = caller_start_time.or_else(|| self.broker.intraday_execution_start_time());
|
||||
let start_time = post_close_window
|
||||
.map(|window| window.0)
|
||||
.or(caller_start_time)
|
||||
.or_else(|| self.broker.intraday_execution_start_time());
|
||||
let end_time = post_close_window.map(|window| window.1).or(caller_end_time);
|
||||
let mut symbols = execution_quote_symbols_for_decision(decision, portfolio, open_orders);
|
||||
self.load_missing_execution_quotes(execution_date, start_time, end_time, &mut symbols)?;
|
||||
|
||||
@@ -4614,15 +4622,16 @@ mod tests {
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use chrono::{NaiveDate, NaiveTime};
|
||||
|
||||
use super::{BacktestConfig, BacktestEngine};
|
||||
use crate::broker::{BrokerSimulator, MatchingType};
|
||||
use crate::broker::{BrokerSimulator, MatchingType, SlippageModel};
|
||||
use crate::cost::ChinaAShareCostModel;
|
||||
use crate::data::{
|
||||
BenchmarkSnapshot, CandidateEligibility, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
|
||||
PriceField,
|
||||
IntradayExecutionQuote, PriceField,
|
||||
};
|
||||
use crate::events::{OrderSide, OrderStatus};
|
||||
use crate::instrument::Instrument;
|
||||
@@ -5302,6 +5311,85 @@ mod tests {
|
||||
.expect("backtest run")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_close_order_at_1500_loads_and_uses_post_close_matching_window() {
|
||||
let date = d(2026, 7, 6);
|
||||
let data = dataset_from_market_and_candidates(
|
||||
vec![market(date, 9.5, 10.0)],
|
||||
vec![candidate(date)],
|
||||
);
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks,
|
||||
PriceField::Close,
|
||||
)
|
||||
.with_matching_type(MatchingType::CurrentBarClose)
|
||||
.with_intraday_execution_start_time(
|
||||
NaiveTime::from_hms_opt(15, 0, 0).expect("valid submission time"),
|
||||
)
|
||||
.with_slippage_model(SlippageModel::PriceRatio(0.25))
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(false)
|
||||
.with_inactive_limit(false);
|
||||
let config = BacktestConfig {
|
||||
initial_cash: 100_000.0,
|
||||
benchmark_code: "000852.SH".to_string(),
|
||||
start_date: Some(date),
|
||||
end_date: Some(date),
|
||||
decision_lag_trading_days: 0,
|
||||
execution_price_field: PriceField::Close,
|
||||
};
|
||||
let requests = Arc::new(Mutex::new(Vec::new()));
|
||||
let captured = Arc::clone(&requests);
|
||||
let mut engine = BacktestEngine::new(
|
||||
data,
|
||||
BuyWhenDecisionDateStrategy {
|
||||
decision_date: date,
|
||||
},
|
||||
broker,
|
||||
config,
|
||||
)
|
||||
.with_execution_quote_loader(move |request| {
|
||||
captured
|
||||
.lock()
|
||||
.expect("request capture lock")
|
||||
.push((request.start_time, request.end_time));
|
||||
Ok(request
|
||||
.symbols
|
||||
.into_iter()
|
||||
.map(|symbol| IntradayExecutionQuote {
|
||||
date: request.date,
|
||||
symbol,
|
||||
timestamp: request.date.and_hms_opt(15, 5, 0).expect("valid timestamp"),
|
||||
last_price: 12.0,
|
||||
bid1: 11.99,
|
||||
ask1: 12.01,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
volume_delta: 10_000,
|
||||
amount_delta: 120_000.0,
|
||||
trading_phase: Some("post_close_fixed_price".to_string()),
|
||||
})
|
||||
.collect())
|
||||
});
|
||||
|
||||
let result = engine.run().expect("post-close backtest run");
|
||||
|
||||
assert_eq!(
|
||||
requests.lock().expect("request capture lock").as_slice(),
|
||||
&[(
|
||||
NaiveTime::from_hms_opt(15, 5, 0),
|
||||
NaiveTime::from_hms_opt(15, 30, 0),
|
||||
)]
|
||||
);
|
||||
assert_eq!(result.fills.len(), 1, "{result:?}");
|
||||
assert_eq!(result.fills[0].price, 10.0);
|
||||
assert_eq!(
|
||||
result.fills[0].execution_timestamp,
|
||||
date.and_hms_opt(15, 5, 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compact_progress_keeps_counts_without_event_payload_clones() {
|
||||
let mut engine = engine_with_matching(MatchingType::CurrentBarClose, PriceField::Close, 0);
|
||||
|
||||
Reference in New Issue
Block a user