支持显式止盈止损参考价口径

This commit is contained in:
boris
2026-07-17 12:07:07 +08:00
parent 8f098e4da1
commit 60457389a3
4 changed files with 249 additions and 46 deletions
+17
View File
@@ -554,6 +554,18 @@ impl AdjustedCloseSeries {
12,
))
}
fn latest_back_adjusted_close(&self, date: NaiveDate) -> Option<f64> {
let index = match self.dates.binary_search(&date) {
Ok(index) => index,
Err(0) => return None,
Err(index) => index - 1,
};
self.back_adjusted_closes
.get(index)
.copied()
.filter(|value| value.is_finite() && *value > 0.0)
}
}
impl SymbolPriceSeries {
@@ -2429,6 +2441,11 @@ impl DataSet {
}
}
pub fn market_latest_back_adjusted_close(&self, date: NaiveDate, symbol: &str) -> Option<f64> {
self.adjusted_close_series(symbol)
.and_then(|series| series.latest_back_adjusted_close(date))
}
pub fn market_decision_numeric_values(
&self,
date: NaiveDate,
+2 -1
View File
@@ -53,7 +53,8 @@ pub use platform_expr_strategy::{
PlatformAccountActionKind, PlatformExplicitActionStage, PlatformExplicitCancelKind,
PlatformExplicitOrderKind, PlatformExprStrategy, PlatformExprStrategyConfig,
PlatformPortfolioDrawdownControlConfig, PlatformRebalanceSchedule, PlatformScheduleFrequency,
PlatformSelectionQuotePlan, PlatformTradeAction, PlatformUniverseActionKind,
PlatformSelectionQuotePlan, PlatformStopTakeReferencePriceMode, PlatformTradeAction,
PlatformUniverseActionKind,
};
pub use platform_runtime_schema::{
PLATFORM_RUNTIME_SCHEMA_VERSION, PlatformRuntimeSchema, reserved_scope_names,
+161 -43
View File
@@ -324,6 +324,12 @@ pub enum PlatformExplicitActionStage {
OnDay,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlatformStopTakeReferencePriceMode {
PositionCostBasis,
SignalDayPostAdjustedClose,
}
#[derive(Debug, Clone)]
pub struct PlatformExprStrategyConfig {
pub strategy_name: String,
@@ -346,6 +352,7 @@ pub struct PlatformExprStrategyConfig {
pub portfolio_drawdown_control: Option<PlatformPortfolioDrawdownControlConfig>,
pub stop_loss_expr: String,
pub take_profit_expr: String,
pub stop_take_reference_price_mode: PlatformStopTakeReferencePriceMode,
pub rank_by: String,
pub rank_expr: String,
pub rank_desc: bool,
@@ -424,6 +431,7 @@ fn band_low(index_close) {
portfolio_drawdown_control: None,
stop_loss_expr: String::new(),
take_profit_expr: String::new(),
stop_take_reference_price_mode: PlatformStopTakeReferencePriceMode::PositionCostBasis,
rank_by: "market_cap".to_string(),
rank_expr: String::new(),
rank_desc: false,
@@ -7824,6 +7832,21 @@ impl PlatformExprStrategy {
}
continue;
}
if self.config.stop_take_reference_price_mode
== PlatformStopTakeReferencePriceMode::SignalDayPostAdjustedClose
&& ctx
.data
.market_latest_back_adjusted_close(date, &candidate.symbol)
.is_none()
{
if diagnostics.len() < 12 {
diagnostics.push(format!(
"{} rejected by missing signal-day post-adjusted close",
candidate.symbol
));
}
continue;
}
selected.push(candidate.symbol.clone());
if selected.len() >= limit {
break;
@@ -8642,13 +8665,33 @@ impl PlatformExprStrategy {
.average_entry_price()
.filter(|value| value.is_finite() && *value > 0.0)
.unwrap_or(position.average_cost);
let stop_take_base_price = if self.config.aiquant_transaction_cost
&& position.average_cost.is_finite()
&& position.average_cost > 0.0
{
position.average_cost
} else {
entry_avg_price
let stop_take_base_price = match self.config.stop_take_reference_price_mode {
PlatformStopTakeReferencePriceMode::PositionCostBasis => {
if self.config.aiquant_transaction_cost
&& position.average_cost.is_finite()
&& position.average_cost > 0.0
{
position.average_cost
} else {
entry_avg_price
}
}
PlatformStopTakeReferencePriceMode::SignalDayPostAdjustedClose => {
let entry_date = self.position_entry_dates.get(symbol).copied().ok_or_else(|| {
BacktestError::Execution(format!(
"missing model admission date for stop/take reference: symbol={symbol}, signal_date={signal_date}"
))
})?;
ctx.data
.market_latest_back_adjusted_close(entry_date, symbol)
.ok_or_else(|| {
BacktestError::Data(crate::data::DataSetError::MissingSnapshot {
kind: "model admission post-adjusted close",
date: entry_date,
symbol: symbol.to_string(),
})
})?
}
};
if position.quantity == 0 || stop_take_base_price <= 0.0 {
return Ok((false, false));
@@ -8776,6 +8819,20 @@ impl PlatformExprStrategy {
symbol: &str,
stock: &StockExpressionState,
) -> Result<f64, BacktestError> {
if self.config.stop_take_reference_price_mode
== PlatformStopTakeReferencePriceMode::SignalDayPostAdjustedClose
{
return ctx
.data
.market_latest_back_adjusted_close(signal_date, symbol)
.ok_or_else(|| {
BacktestError::Data(crate::data::DataSetError::MissingSnapshot {
kind: "post-adjusted close",
date: signal_date,
symbol: symbol.to_string(),
})
});
}
if self.config.aiquant_transaction_cost
&& self.config.matching_type == MatchingType::NextBarOpen
{
@@ -10367,9 +10424,9 @@ mod tests {
PlatformAccountActionKind, PlatformExplicitActionStage, PlatformExplicitCancelKind,
PlatformExplicitOrderKind, PlatformExprStrategy, PlatformExprStrategyConfig,
PlatformPortfolioDrawdownControlConfig, PlatformPortfolioDrawdownController,
PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformTradeAction,
PlatformUniverseActionKind, SelectionRiskDeferral, StockFilterQuoteUsage,
precomputed_stock_rolling_mean,
PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformStopTakeReferencePriceMode,
PlatformTradeAction, PlatformUniverseActionKind, SelectionRiskDeferral,
StockFilterQuoteUsage, precomputed_stock_rolling_mean,
};
use crate::{
AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction,
@@ -14214,39 +14271,70 @@ mod tests {
delisted_at: None,
status: "active".to_string(),
}],
vec![DailyMarketSnapshot {
date,
symbol: symbol.to_string(),
timestamp: Some("2025-03-14 10:18:00".to_string()),
day_open: 9.30,
open: 9.30,
high: 9.35,
low: 9.18,
close: 9.20,
last_price: 9.20,
bid1: 9.20,
ask1: 9.21,
prev_close: 10.00,
volume: 1_000_000,
minute_volume: 1_000,
bid1_volume: 1_000,
ask1_volume: 1_000,
trading_phase: Some("continuous".to_string()),
paused: false,
upper_limit: 11.00,
lower_limit: 9.00,
price_tick: 0.01,
}],
vec![DailyFactorSnapshot {
date,
symbol: symbol.to_string(),
market_cap_bn: 3.2,
free_float_cap_bn: 2.1,
pe_ttm: 8.0,
turnover_ratio: Some(3.0),
effective_turnover_ratio: Some(3.0),
extra_factors: BTreeMap::new(),
}],
vec![
DailyMarketSnapshot {
date: prev_date,
symbol: symbol.to_string(),
timestamp: Some("2025-03-13 15:00:00".to_string()),
day_open: 10.00,
open: 10.00,
high: 10.10,
low: 9.90,
close: 10.00,
last_price: 10.00,
bid1: 9.99,
ask1: 10.00,
prev_close: 10.00,
volume: 1_000_000,
minute_volume: 1_000,
bid1_volume: 1_000,
ask1_volume: 1_000,
trading_phase: Some("close".to_string()),
paused: false,
upper_limit: 11.00,
lower_limit: 9.00,
price_tick: 0.01,
},
DailyMarketSnapshot {
date,
symbol: symbol.to_string(),
timestamp: Some("2025-03-14 10:18:00".to_string()),
day_open: 9.30,
open: 9.30,
high: 9.35,
low: 9.18,
close: 9.30,
last_price: 9.20,
bid1: 9.20,
ask1: 9.21,
prev_close: 10.00,
volume: 1_000_000,
minute_volume: 1_000,
bid1_volume: 1_000,
ask1_volume: 1_000,
trading_phase: Some("continuous".to_string()),
paused: false,
upper_limit: 11.00,
lower_limit: 9.00,
price_tick: 0.01,
},
],
[prev_date, date]
.into_iter()
.map(|factor_date| DailyFactorSnapshot {
date: factor_date,
symbol: symbol.to_string(),
market_cap_bn: 3.2,
free_float_cap_bn: 2.1,
pe_ttm: 8.0,
turnover_ratio: Some(3.0),
effective_turnover_ratio: Some(3.0),
extra_factors: BTreeMap::from([(
"adjustment_factor_backward1".to_string(),
1.0,
)]),
})
.collect(),
vec![CandidateEligibility {
date,
symbol: symbol.to_string(),
@@ -14335,6 +14423,36 @@ mod tests {
"{:?}",
decision.order_intents
);
let mut signal_price_cfg = PlatformExprStrategyConfig::microcap_rotation();
signal_price_cfg.rotation_enabled = false;
signal_price_cfg.aiquant_transaction_cost = true;
signal_price_cfg.intraday_execution_time =
Some(NaiveTime::from_hms_opt(10, 18, 0).expect("time"));
signal_price_cfg.signal_symbol = symbol.to_string();
signal_price_cfg.stop_loss_expr = "0.92".to_string();
signal_price_cfg.take_profit_expr.clear();
signal_price_cfg.stop_take_reference_price_mode =
PlatformStopTakeReferencePriceMode::SignalDayPostAdjustedClose;
let mut signal_price_strategy = PlatformExprStrategy::new(signal_price_cfg);
signal_price_strategy.remember_position_entry_date(symbol, prev_date);
let signal_price_decision = signal_price_strategy
.on_day(&ctx)
.expect("signal price platform decision");
assert!(
!signal_price_decision.order_intents.iter().any(|intent| matches!(
intent,
OrderIntent::TargetValue {
symbol: intent_symbol,
target_value,
reason,
} if intent_symbol == symbol && *target_value == 0.0 && reason == "stop_loss_exit"
)),
"{:?}",
signal_price_decision.order_intents
);
}
#[test]
+69 -2
View File
@@ -8,8 +8,8 @@ use crate::{
DynamicSlippageConfig, MatchingType, PlatformAccountActionKind, PlatformExplicitActionStage,
PlatformExplicitCancelKind, PlatformExplicitOrderKind, PlatformExprStrategyConfig,
PlatformPortfolioDrawdownControlConfig, PlatformRebalanceSchedule, PlatformScheduleFrequency,
PlatformTradeAction, PlatformUniverseActionKind, RebalanceCashMode, ScheduleTimeRule,
SlippageModel,
PlatformStopTakeReferencePriceMode, PlatformTradeAction, PlatformUniverseActionKind,
RebalanceCashMode, ScheduleTimeRule, SlippageModel,
};
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
@@ -669,6 +669,12 @@ pub struct StrategyExpressionRiskConfig {
pub stop_loss_expr: Option<String>,
#[serde(default)]
pub take_profit_expr: Option<String>,
#[serde(
default,
alias = "referencePriceMode",
alias = "stop_take_reference_price_mode"
)]
pub stop_take_reference_price_mode: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
@@ -849,6 +855,24 @@ fn is_aiquant_profile(value: Option<&str>) -> bool {
.is_some_and(|item| item == "aiquant" || item == "aiquant_rqalpha" || item == "rqalpha")
}
fn parse_stop_take_reference_price_mode(
value: &str,
) -> Result<PlatformStopTakeReferencePriceMode, String> {
match value.trim().to_ascii_lowercase().replace('-', "_").as_str() {
"position_cost_basis" | "position_cost" | "execution_cost_basis" => {
Ok(PlatformStopTakeReferencePriceMode::PositionCostBasis)
}
"signal_day_post_adjusted_close"
| "signal_post_adjusted_close"
| "model_signal_post_adjusted_close" => {
Ok(PlatformStopTakeReferencePriceMode::SignalDayPostAdjustedClose)
}
other => Err(format!(
"runtimeExpressions.risk.stopTakeReferencePriceMode unsupported: {other}"
)),
}
}
fn apply_cost_overrides(
cfg: &mut PlatformExprStrategyConfig,
commission_rate: Option<f64>,
@@ -1667,6 +1691,14 @@ pub fn platform_expr_config_from_spec(
{
cfg.take_profit_expr = expr.clone();
}
if let Some(mode) = risk
.stop_take_reference_price_mode
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
cfg.stop_take_reference_price_mode = parse_stop_take_reference_price_mode(mode)?;
}
}
if let Some(ordering) = runtime_expr.ordering.as_ref() {
if let Some(rank_by) = ordering
@@ -3367,4 +3399,39 @@ mod tests {
let error = platform_expr_config_from_value("", "", &spec).expect_err("invalid trigger");
assert!(error.to_string().contains("drawdownTrigger"));
}
#[test]
fn parses_signal_day_post_adjusted_stop_take_reference_price_mode() {
let spec = serde_json::json!({
"runtimeExpressions": {
"risk": {
"stopLossExpr": "0.92",
"takeProfitExpr": "1.16",
"stopTakeReferencePriceMode": "signal_day_post_adjusted_close"
}
}
});
let cfg = platform_expr_config_from_value("", "", &spec).expect("config");
assert_eq!(
cfg.stop_take_reference_price_mode,
PlatformStopTakeReferencePriceMode::SignalDayPostAdjustedClose
);
}
#[test]
fn rejects_unknown_stop_take_reference_price_mode() {
let spec = serde_json::json!({
"runtimeExpressions": {
"risk": {
"stopTakeReferencePriceMode": "future_price"
}
}
});
let error = platform_expr_config_from_value("", "", &spec).expect_err("invalid mode");
assert!(error.to_string().contains("stopTakeReferencePriceMode"));
}
}