修复盘中止盈止损行情缓存
This commit is contained in:
@@ -1263,8 +1263,9 @@ pub struct PlatformExprStrategy {
|
|||||||
stock_text_factors_required: bool,
|
stock_text_factors_required: bool,
|
||||||
stock_state_cache_date: RefCell<Option<NaiveDate>>,
|
stock_state_cache_date: RefCell<Option<NaiveDate>>,
|
||||||
stock_state_cache_calendar_index: RefCell<Option<usize>>,
|
stock_state_cache_calendar_index: RefCell<Option<usize>>,
|
||||||
stock_state_cache:
|
stock_state_cache: RefCell<
|
||||||
RefCell<AHashMap<(NaiveDate, u32, Option<NaiveTime>, bool), Arc<StockExpressionState>>>,
|
AHashMap<(NaiveDate, u32, Option<NaiveTime>, bool, bool), Arc<StockExpressionState>>,
|
||||||
|
>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@@ -4329,7 +4330,17 @@ impl PlatformExprStrategy {
|
|||||||
S: StockStateSnapshotSource<'a>,
|
S: StockStateSnapshotSource<'a>,
|
||||||
{
|
{
|
||||||
let calendar_index = self.prepare_stock_state_cache_date(ctx, date);
|
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) {
|
if let Some(state) = self.stock_state_cache.borrow().get(&cache_key) {
|
||||||
return Ok(Arc::clone(state));
|
return Ok(Arc::clone(state));
|
||||||
}
|
}
|
||||||
@@ -11573,14 +11584,6 @@ impl PlatformExprStrategy {
|
|||||||
}
|
}
|
||||||
let scheduled_time = self.intraday_execution_start_time();
|
let scheduled_time = self.intraday_execution_start_time();
|
||||||
if self.uses_intraday_execution_quotes()
|
if self.uses_intraday_execution_quotes()
|
||||||
&& matches!(
|
|
||||||
self.config.matching_type,
|
|
||||||
MatchingType::MinuteLast
|
|
||||||
| MatchingType::MinuteBestOwn
|
|
||||||
| MatchingType::MinuteBestCounterparty
|
|
||||||
| MatchingType::Vwap
|
|
||||||
| MatchingType::Twap
|
|
||||||
)
|
|
||||||
&& self
|
&& self
|
||||||
.scheduled_quote_at_time(ctx, signal_date, symbol, Some(scheduled_time))
|
.scheduled_quote_at_time(ctx, signal_date, symbol, Some(scheduled_time))
|
||||||
.is_none()
|
.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("e_ctx, date).expect("day state");
|
||||||
|
assert_eq!(
|
||||||
|
strategy
|
||||||
|
.stop_take_action("e_ctx, date, date, "e_day, symbol)
|
||||||
|
.expect("quoted stop check"),
|
||||||
|
(true, false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn platform_stop_loss_residual_pending_rechecks_signal_next_day() {
|
fn platform_stop_loss_residual_pending_rechecks_signal_next_day() {
|
||||||
let prev_date = d(2025, 1, 6);
|
let prev_date = d(2025, 1, 6);
|
||||||
|
|||||||
Reference in New Issue
Block a user