修复盘中止盈止损行情缓存

This commit is contained in:
boris
2026-09-07 04:17:37 +08:00
parent 27e523a1dc
commit 78e872b609
+160 -11
View File
@@ -1263,8 +1263,9 @@ pub struct PlatformExprStrategy {
stock_text_factors_required: bool,
stock_state_cache_date: RefCell<Option<NaiveDate>>,
stock_state_cache_calendar_index: RefCell<Option<usize>>,
stock_state_cache:
RefCell<AHashMap<(NaiveDate, u32, Option<NaiveTime>, bool), Arc<StockExpressionState>>>,
stock_state_cache: RefCell<
AHashMap<(NaiveDate, u32, Option<NaiveTime>, bool, bool), Arc<StockExpressionState>>,
>,
}
#[derive(Debug, Clone, PartialEq)]
@@ -4329,7 +4330,17 @@ impl PlatformExprStrategy {
S: StockStateSnapshotSource<'a>,
{
let calendar_index = self.prepare_stock_state_cache_date(ctx, date);
let cache_key = (factor_date, symbol_id, execution_time, use_intraday_quote);
let has_intraday_quote = use_intraday_quote
&& self
.scheduled_quote_at_time(ctx, date, symbol, execution_time)
.is_some();
let cache_key = (
factor_date,
symbol_id,
execution_time,
use_intraday_quote,
has_intraday_quote,
);
if let Some(state) = self.stock_state_cache.borrow().get(&cache_key) {
return Ok(Arc::clone(state));
}
@@ -11573,14 +11584,6 @@ impl PlatformExprStrategy {
}
let scheduled_time = self.intraday_execution_start_time();
if self.uses_intraday_execution_quotes()
&& matches!(
self.config.matching_type,
MatchingType::MinuteLast
| MatchingType::MinuteBestOwn
| MatchingType::MinuteBestCounterparty
| MatchingType::Vwap
| MatchingType::Twap
)
&& self
.scheduled_quote_at_time(ctx, signal_date, symbol, Some(scheduled_time))
.is_none()
@@ -18685,6 +18688,152 @@ mod tests {
);
}
#[test]
fn timed_current_close_stop_loss_reloads_state_after_quote_arrives() {
let prev_date = d(2025, 10, 10);
let date = d(2025, 10, 13);
let symbol = "603726.SH";
let instrument = Instrument {
symbol: symbol.to_string(),
name: symbol.to_string(),
board: "SH".to_string(),
round_lot: 100,
listed_at: Some(d(2020, 1, 1)),
delisted_at: None,
status: "active".to_string(),
};
let market = DailyMarketSnapshot {
date,
symbol: symbol.to_string(),
timestamp: Some("2025-10-13 15:00:00".to_string()),
day_open: 23.46,
open: 23.46,
high: 26.20,
low: 23.20,
close: 25.90,
last_price: 25.90,
bid1: 25.89,
ask1: 25.90,
prev_close: 26.07,
volume: 1_000_000,
minute_volume: 10_000,
bid1_volume: 10_000,
ask1_volume: 10_000,
trading_phase: Some("continuous".to_string()),
paused: false,
upper_limit: 28.68,
lower_limit: 23.46,
price_tick: 0.01,
};
let factor = DailyFactorSnapshot {
date,
symbol: symbol.to_string(),
market_cap_bn: 10.0,
free_float_cap_bn: 8.0,
pe_ttm: 8.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
extra_factors: BTreeMap::new(),
};
let candidate = CandidateEligibility {
date,
symbol: symbol.to_string(),
is_st: false,
is_star_st: false,
is_new_listing: false,
is_paused: false,
allow_buy: true,
allow_sell: true,
is_kcb: false,
is_one_yuan: false,
risk_level_code: None,
};
let benchmark = BenchmarkSnapshot {
date,
benchmark: "000300.SH".to_string(),
open: 4000.0,
close: 4010.0,
prev_close: 3990.0,
volume: 1_000_000,
};
let without_quote = DataSet::from_components(
vec![instrument.clone()],
vec![market.clone()],
vec![factor.clone()],
vec![candidate.clone()],
vec![benchmark.clone()],
)
.expect("dataset without quote");
let with_quote = DataSet::from_components_with_actions_and_quotes(
vec![instrument],
vec![market],
vec![factor],
vec![candidate],
vec![benchmark],
Vec::new(),
vec![IntradayExecutionQuote {
date,
symbol: symbol.to_string(),
timestamp: date.and_hms_opt(9, 30, 0).expect("timestamp"),
last_price: 23.46,
bid1: 23.45,
ask1: 23.46,
bid1_volume: 10_000,
ask1_volume: 10_000,
volume_delta: 10_000,
amount_delta: 234_600.0,
trading_phase: Some("continuous".to_string()),
}],
)
.expect("dataset with quote");
let mut portfolio = PortfolioState::new(1_000_000.0);
portfolio
.position_mut(symbol)
.buy(prev_date, 16_400, 26.700856097560973);
let subscriptions = BTreeSet::new();
let context = |data| StrategyContext {
execution_date: date,
decision_date: date,
decision_index: 40,
data,
portfolio: &portfolio,
futures_account: None,
open_orders: &[],
dynamic_universe: None,
subscriptions: &subscriptions,
process_events: &[],
active_process_event: None,
active_datetime: None,
order_events: &[],
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.rotation_enabled = false;
cfg.matching_type = MatchingType::CurrentBarClose;
cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(9, 30, 0).expect("time"));
cfg.signal_symbol = symbol.to_string();
cfg.stop_loss_expr = "holding_return <= -0.08".to_string();
cfg.take_profit_expr.clear();
let strategy = PlatformExprStrategy::new(cfg);
let no_quote_ctx = context(&without_quote);
let no_quote_day = strategy.day_state(&no_quote_ctx, date).expect("day state");
assert_eq!(
strategy
.stop_take_action(&no_quote_ctx, date, date, &no_quote_day, symbol)
.expect("no quote stop check"),
(false, false),
);
let quote_ctx = context(&with_quote);
let quote_day = strategy.day_state(&quote_ctx, date).expect("day state");
assert_eq!(
strategy
.stop_take_action(&quote_ctx, date, date, &quote_day, symbol)
.expect("quoted stop check"),
(true, false),
);
}
#[test]
fn platform_stop_loss_residual_pending_rechecks_signal_next_day() {
let prev_date = d(2025, 1, 6);