支持显式止盈止损参考价口径
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user