支持显式止盈止损参考价口径
This commit is contained in:
@@ -554,6 +554,18 @@ impl AdjustedCloseSeries {
|
|||||||
12,
|
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 {
|
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(
|
pub fn market_decision_numeric_values(
|
||||||
&self,
|
&self,
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ pub use platform_expr_strategy::{
|
|||||||
PlatformAccountActionKind, PlatformExplicitActionStage, PlatformExplicitCancelKind,
|
PlatformAccountActionKind, PlatformExplicitActionStage, PlatformExplicitCancelKind,
|
||||||
PlatformExplicitOrderKind, PlatformExprStrategy, PlatformExprStrategyConfig,
|
PlatformExplicitOrderKind, PlatformExprStrategy, PlatformExprStrategyConfig,
|
||||||
PlatformPortfolioDrawdownControlConfig, PlatformRebalanceSchedule, PlatformScheduleFrequency,
|
PlatformPortfolioDrawdownControlConfig, PlatformRebalanceSchedule, PlatformScheduleFrequency,
|
||||||
PlatformSelectionQuotePlan, PlatformTradeAction, PlatformUniverseActionKind,
|
PlatformSelectionQuotePlan, PlatformStopTakeReferencePriceMode, PlatformTradeAction,
|
||||||
|
PlatformUniverseActionKind,
|
||||||
};
|
};
|
||||||
pub use platform_runtime_schema::{
|
pub use platform_runtime_schema::{
|
||||||
PLATFORM_RUNTIME_SCHEMA_VERSION, PlatformRuntimeSchema, reserved_scope_names,
|
PLATFORM_RUNTIME_SCHEMA_VERSION, PlatformRuntimeSchema, reserved_scope_names,
|
||||||
|
|||||||
@@ -324,6 +324,12 @@ pub enum PlatformExplicitActionStage {
|
|||||||
OnDay,
|
OnDay,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum PlatformStopTakeReferencePriceMode {
|
||||||
|
PositionCostBasis,
|
||||||
|
SignalDayPostAdjustedClose,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PlatformExprStrategyConfig {
|
pub struct PlatformExprStrategyConfig {
|
||||||
pub strategy_name: String,
|
pub strategy_name: String,
|
||||||
@@ -346,6 +352,7 @@ pub struct PlatformExprStrategyConfig {
|
|||||||
pub portfolio_drawdown_control: Option<PlatformPortfolioDrawdownControlConfig>,
|
pub portfolio_drawdown_control: Option<PlatformPortfolioDrawdownControlConfig>,
|
||||||
pub stop_loss_expr: String,
|
pub stop_loss_expr: String,
|
||||||
pub take_profit_expr: String,
|
pub take_profit_expr: String,
|
||||||
|
pub stop_take_reference_price_mode: PlatformStopTakeReferencePriceMode,
|
||||||
pub rank_by: String,
|
pub rank_by: String,
|
||||||
pub rank_expr: String,
|
pub rank_expr: String,
|
||||||
pub rank_desc: bool,
|
pub rank_desc: bool,
|
||||||
@@ -424,6 +431,7 @@ fn band_low(index_close) {
|
|||||||
portfolio_drawdown_control: None,
|
portfolio_drawdown_control: None,
|
||||||
stop_loss_expr: String::new(),
|
stop_loss_expr: String::new(),
|
||||||
take_profit_expr: String::new(),
|
take_profit_expr: String::new(),
|
||||||
|
stop_take_reference_price_mode: PlatformStopTakeReferencePriceMode::PositionCostBasis,
|
||||||
rank_by: "market_cap".to_string(),
|
rank_by: "market_cap".to_string(),
|
||||||
rank_expr: String::new(),
|
rank_expr: String::new(),
|
||||||
rank_desc: false,
|
rank_desc: false,
|
||||||
@@ -7824,6 +7832,21 @@ impl PlatformExprStrategy {
|
|||||||
}
|
}
|
||||||
continue;
|
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());
|
selected.push(candidate.symbol.clone());
|
||||||
if selected.len() >= limit {
|
if selected.len() >= limit {
|
||||||
break;
|
break;
|
||||||
@@ -8642,13 +8665,33 @@ impl PlatformExprStrategy {
|
|||||||
.average_entry_price()
|
.average_entry_price()
|
||||||
.filter(|value| value.is_finite() && *value > 0.0)
|
.filter(|value| value.is_finite() && *value > 0.0)
|
||||||
.unwrap_or(position.average_cost);
|
.unwrap_or(position.average_cost);
|
||||||
let stop_take_base_price = if self.config.aiquant_transaction_cost
|
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.is_finite()
|
||||||
&& position.average_cost > 0.0
|
&& position.average_cost > 0.0
|
||||||
{
|
{
|
||||||
position.average_cost
|
position.average_cost
|
||||||
} else {
|
} else {
|
||||||
entry_avg_price
|
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 {
|
if position.quantity == 0 || stop_take_base_price <= 0.0 {
|
||||||
return Ok((false, false));
|
return Ok((false, false));
|
||||||
@@ -8776,6 +8819,20 @@ impl PlatformExprStrategy {
|
|||||||
symbol: &str,
|
symbol: &str,
|
||||||
stock: &StockExpressionState,
|
stock: &StockExpressionState,
|
||||||
) -> Result<f64, BacktestError> {
|
) -> 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
|
if self.config.aiquant_transaction_cost
|
||||||
&& self.config.matching_type == MatchingType::NextBarOpen
|
&& self.config.matching_type == MatchingType::NextBarOpen
|
||||||
{
|
{
|
||||||
@@ -10367,9 +10424,9 @@ mod tests {
|
|||||||
PlatformAccountActionKind, PlatformExplicitActionStage, PlatformExplicitCancelKind,
|
PlatformAccountActionKind, PlatformExplicitActionStage, PlatformExplicitCancelKind,
|
||||||
PlatformExplicitOrderKind, PlatformExprStrategy, PlatformExprStrategyConfig,
|
PlatformExplicitOrderKind, PlatformExprStrategy, PlatformExprStrategyConfig,
|
||||||
PlatformPortfolioDrawdownControlConfig, PlatformPortfolioDrawdownController,
|
PlatformPortfolioDrawdownControlConfig, PlatformPortfolioDrawdownController,
|
||||||
PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformTradeAction,
|
PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformStopTakeReferencePriceMode,
|
||||||
PlatformUniverseActionKind, SelectionRiskDeferral, StockFilterQuoteUsage,
|
PlatformTradeAction, PlatformUniverseActionKind, SelectionRiskDeferral,
|
||||||
precomputed_stock_rolling_mean,
|
StockFilterQuoteUsage, precomputed_stock_rolling_mean,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction,
|
AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction,
|
||||||
@@ -14214,7 +14271,31 @@ mod tests {
|
|||||||
delisted_at: None,
|
delisted_at: None,
|
||||||
status: "active".to_string(),
|
status: "active".to_string(),
|
||||||
}],
|
}],
|
||||||
vec![DailyMarketSnapshot {
|
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,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: Some("2025-03-14 10:18:00".to_string()),
|
timestamp: Some("2025-03-14 10:18:00".to_string()),
|
||||||
@@ -14222,7 +14303,7 @@ mod tests {
|
|||||||
open: 9.30,
|
open: 9.30,
|
||||||
high: 9.35,
|
high: 9.35,
|
||||||
low: 9.18,
|
low: 9.18,
|
||||||
close: 9.20,
|
close: 9.30,
|
||||||
last_price: 9.20,
|
last_price: 9.20,
|
||||||
bid1: 9.20,
|
bid1: 9.20,
|
||||||
ask1: 9.21,
|
ask1: 9.21,
|
||||||
@@ -14236,17 +14317,24 @@ mod tests {
|
|||||||
upper_limit: 11.00,
|
upper_limit: 11.00,
|
||||||
lower_limit: 9.00,
|
lower_limit: 9.00,
|
||||||
price_tick: 0.01,
|
price_tick: 0.01,
|
||||||
}],
|
},
|
||||||
vec![DailyFactorSnapshot {
|
],
|
||||||
date,
|
[prev_date, date]
|
||||||
|
.into_iter()
|
||||||
|
.map(|factor_date| DailyFactorSnapshot {
|
||||||
|
date: factor_date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
market_cap_bn: 3.2,
|
market_cap_bn: 3.2,
|
||||||
free_float_cap_bn: 2.1,
|
free_float_cap_bn: 2.1,
|
||||||
pe_ttm: 8.0,
|
pe_ttm: 8.0,
|
||||||
turnover_ratio: Some(3.0),
|
turnover_ratio: Some(3.0),
|
||||||
effective_turnover_ratio: Some(3.0),
|
effective_turnover_ratio: Some(3.0),
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: BTreeMap::from([(
|
||||||
}],
|
"adjustment_factor_backward1".to_string(),
|
||||||
|
1.0,
|
||||||
|
)]),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
vec![CandidateEligibility {
|
vec![CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
@@ -14335,6 +14423,36 @@ mod tests {
|
|||||||
"{:?}",
|
"{:?}",
|
||||||
decision.order_intents
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use crate::{
|
|||||||
DynamicSlippageConfig, MatchingType, PlatformAccountActionKind, PlatformExplicitActionStage,
|
DynamicSlippageConfig, MatchingType, PlatformAccountActionKind, PlatformExplicitActionStage,
|
||||||
PlatformExplicitCancelKind, PlatformExplicitOrderKind, PlatformExprStrategyConfig,
|
PlatformExplicitCancelKind, PlatformExplicitOrderKind, PlatformExprStrategyConfig,
|
||||||
PlatformPortfolioDrawdownControlConfig, PlatformRebalanceSchedule, PlatformScheduleFrequency,
|
PlatformPortfolioDrawdownControlConfig, PlatformRebalanceSchedule, PlatformScheduleFrequency,
|
||||||
PlatformTradeAction, PlatformUniverseActionKind, RebalanceCashMode, ScheduleTimeRule,
|
PlatformStopTakeReferencePriceMode, PlatformTradeAction, PlatformUniverseActionKind,
|
||||||
SlippageModel,
|
RebalanceCashMode, ScheduleTimeRule, SlippageModel,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||||
@@ -669,6 +669,12 @@ pub struct StrategyExpressionRiskConfig {
|
|||||||
pub stop_loss_expr: Option<String>,
|
pub stop_loss_expr: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub take_profit_expr: Option<String>,
|
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)]
|
#[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")
|
.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(
|
fn apply_cost_overrides(
|
||||||
cfg: &mut PlatformExprStrategyConfig,
|
cfg: &mut PlatformExprStrategyConfig,
|
||||||
commission_rate: Option<f64>,
|
commission_rate: Option<f64>,
|
||||||
@@ -1667,6 +1691,14 @@ pub fn platform_expr_config_from_spec(
|
|||||||
{
|
{
|
||||||
cfg.take_profit_expr = expr.clone();
|
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(ordering) = runtime_expr.ordering.as_ref() {
|
||||||
if let Some(rank_by) = ordering
|
if let Some(rank_by) = ordering
|
||||||
@@ -3367,4 +3399,39 @@ mod tests {
|
|||||||
let error = platform_expr_config_from_value("", "", &spec).expect_err("invalid trigger");
|
let error = platform_expr_config_from_value("", "", &spec).expect_err("invalid trigger");
|
||||||
assert!(error.to_string().contains("drawdownTrigger"));
|
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