支持执行日行情价格映射表达式
This commit is contained in:
@@ -884,6 +884,32 @@ impl PlatformExprStrategy {
|
||||
}
|
||||
|
||||
fn is_reserved_scope_name(name: &str) -> bool {
|
||||
if let Some(field) = name
|
||||
.strip_prefix("decision_")
|
||||
.or_else(|| name.strip_prefix("execution_"))
|
||||
{
|
||||
if matches!(
|
||||
field,
|
||||
"day_open"
|
||||
| "open"
|
||||
| "high"
|
||||
| "low"
|
||||
| "close"
|
||||
| "last_price"
|
||||
| "prev_close"
|
||||
| "upper_limit"
|
||||
| "lower_limit"
|
||||
| "volume"
|
||||
| "minute_volume"
|
||||
| "bid1"
|
||||
| "ask1"
|
||||
| "bid1_volume"
|
||||
| "ask1_volume"
|
||||
| "price_tick"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
matches!(
|
||||
name,
|
||||
"signal_close"
|
||||
@@ -3166,6 +3192,7 @@ impl PlatformExprStrategy {
|
||||
day: &DayExpressionState,
|
||||
stock: Option<&StockExpressionState>,
|
||||
position: Option<&PositionExpressionState>,
|
||||
identifiers: &BTreeSet<String>,
|
||||
include_day_factors: bool,
|
||||
include_factors_map: bool,
|
||||
include_process_event_counts: bool,
|
||||
@@ -3183,6 +3210,41 @@ impl PlatformExprStrategy {
|
||||
scope.push("signal_close", day.signal_close);
|
||||
scope.push("benchmark_open", day.benchmark_open);
|
||||
scope.push("benchmark_close", day.benchmark_close);
|
||||
for field in [
|
||||
"day_open",
|
||||
"open",
|
||||
"high",
|
||||
"low",
|
||||
"close",
|
||||
"last_price",
|
||||
"prev_close",
|
||||
"upper_limit",
|
||||
"lower_limit",
|
||||
"volume",
|
||||
"minute_volume",
|
||||
"bid1",
|
||||
"ask1",
|
||||
"bid1_volume",
|
||||
"ask1_volume",
|
||||
"price_tick",
|
||||
] {
|
||||
self.push_market_scope_map(
|
||||
&mut scope,
|
||||
ctx,
|
||||
ctx.decision_date,
|
||||
"decision",
|
||||
field,
|
||||
identifiers,
|
||||
);
|
||||
self.push_market_scope_map(
|
||||
&mut scope,
|
||||
ctx,
|
||||
ctx.execution_date,
|
||||
"execution",
|
||||
field,
|
||||
identifiers,
|
||||
);
|
||||
}
|
||||
scope.push("signal_ma5", day.signal_ma5);
|
||||
scope.push("signal_ma10", day.signal_ma10);
|
||||
scope.push("signal_ma20", day.signal_ma20);
|
||||
@@ -3823,6 +3885,7 @@ impl PlatformExprStrategy {
|
||||
day,
|
||||
stock,
|
||||
position,
|
||||
&normalized_identifiers,
|
||||
include_day_factors,
|
||||
include_factors_map,
|
||||
include_process_event_counts,
|
||||
@@ -5670,14 +5733,15 @@ impl PlatformExprStrategy {
|
||||
if trimmed.is_empty() {
|
||||
return Ok(BTreeMap::new());
|
||||
}
|
||||
let inner = trimmed
|
||||
let Some(inner) = trimmed
|
||||
.strip_prefix('{')
|
||||
.and_then(|value| value.strip_suffix('}'))
|
||||
.ok_or_else(|| {
|
||||
BacktestError::Execution(format!(
|
||||
"platform float map expr must use {{...}} object literal syntax: {trimmed}"
|
||||
))
|
||||
})?;
|
||||
else {
|
||||
return Self::dynamic_to_float_map(
|
||||
self.eval_dynamic(ctx, trimmed, day, stock, position)?,
|
||||
trimmed,
|
||||
);
|
||||
};
|
||||
let mut output = BTreeMap::new();
|
||||
for entry in Self::split_top_level_args(inner) {
|
||||
let Some((raw_key, raw_value)) = Self::split_top_level_key_value(&entry) else {
|
||||
@@ -5711,6 +5775,11 @@ impl PlatformExprStrategy {
|
||||
self.eval_float_map_expr(ctx, trimmed, day, stock, position)?,
|
||||
));
|
||||
}
|
||||
if !trimmed.contains('(') {
|
||||
return Ok(TargetPortfolioOrderPricing::LimitPrices(
|
||||
self.eval_float_map_expr(ctx, trimmed, day, stock, position)?,
|
||||
));
|
||||
}
|
||||
self.eval_algo_order_pricing_expr(ctx, trimmed, day, stock, position)
|
||||
}
|
||||
|
||||
@@ -5850,6 +5919,82 @@ impl PlatformExprStrategy {
|
||||
)))
|
||||
}
|
||||
|
||||
fn dynamic_to_float_map(
|
||||
value: Dynamic,
|
||||
expr: &str,
|
||||
) -> Result<BTreeMap<String, f64>, BacktestError> {
|
||||
let Some(map) = value.try_cast::<Map>() else {
|
||||
return Err(BacktestError::Execution(format!(
|
||||
"platform float map expr must evaluate to a map or use {{...}} object literal syntax: {expr}"
|
||||
)));
|
||||
};
|
||||
let mut output = BTreeMap::new();
|
||||
for (key, value) in map {
|
||||
let number = if let Some(number) = value.clone().try_cast::<f64>() {
|
||||
number
|
||||
} else if let Some(number) = value.clone().try_cast::<i64>() {
|
||||
number as f64
|
||||
} else if let Some(boolean) = value.try_cast::<bool>() {
|
||||
if boolean { 1.0 } else { 0.0 }
|
||||
} else {
|
||||
return Err(BacktestError::Execution(format!(
|
||||
"platform float map expr produced non-numeric value for key {key}: {expr}"
|
||||
)));
|
||||
};
|
||||
output.insert(key.to_string(), number);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn market_scope_value(snapshot: &DailyMarketSnapshot, field: &str) -> Option<f64> {
|
||||
let value = match field {
|
||||
"day_open" => snapshot.day_open,
|
||||
"open" => snapshot.open,
|
||||
"high" => snapshot.high,
|
||||
"low" => snapshot.low,
|
||||
"close" => snapshot.close,
|
||||
"last_price" => snapshot.last_price,
|
||||
"prev_close" => snapshot.prev_close,
|
||||
"upper_limit" => snapshot.upper_limit,
|
||||
"lower_limit" => snapshot.lower_limit,
|
||||
"volume" => snapshot.volume as f64,
|
||||
"minute_volume" => snapshot.minute_volume as f64,
|
||||
"bid1" => snapshot.bid1,
|
||||
"ask1" => snapshot.ask1,
|
||||
"bid1_volume" => snapshot.bid1_volume as f64,
|
||||
"ask1_volume" => snapshot.ask1_volume as f64,
|
||||
"price_tick" => snapshot.price_tick,
|
||||
_ => return None,
|
||||
};
|
||||
value.is_finite().then_some(value)
|
||||
}
|
||||
|
||||
fn scope_identifier_requested(&self, identifiers: &BTreeSet<String>, name: &str) -> bool {
|
||||
identifiers.contains(name) || self.prelude_identifier_candidates.contains(name)
|
||||
}
|
||||
|
||||
fn push_market_scope_map(
|
||||
&self,
|
||||
scope: &mut Scope<'_>,
|
||||
ctx: &StrategyContext<'_>,
|
||||
date: NaiveDate,
|
||||
prefix: &str,
|
||||
field: &str,
|
||||
identifiers: &BTreeSet<String>,
|
||||
) {
|
||||
let name = format!("{prefix}_{field}");
|
||||
if !self.scope_identifier_requested(identifiers, &name) {
|
||||
return;
|
||||
}
|
||||
let mut map = Map::new();
|
||||
for snapshot in ctx.data.market_snapshots_on(date) {
|
||||
if let Some(value) = Self::market_scope_value(snapshot, field) {
|
||||
map.insert(snapshot.symbol.clone().into(), Dynamic::from(value));
|
||||
}
|
||||
}
|
||||
scope.push_dynamic(name, Dynamic::from(map));
|
||||
}
|
||||
|
||||
fn action_stock_state(
|
||||
&self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
@@ -27077,6 +27222,52 @@ mod tests {
|
||||
lower_limit: 900.9,
|
||||
price_tick: 0.01,
|
||||
},
|
||||
DailyMarketSnapshot {
|
||||
date: execution_date,
|
||||
symbol: "000521.SZ".to_string(),
|
||||
timestamp: Some("2023-01-04 09:30:00".to_string()),
|
||||
day_open: 12.34,
|
||||
open: 12.31,
|
||||
high: 12.80,
|
||||
low: 12.10,
|
||||
close: 12.70,
|
||||
last_price: 12.70,
|
||||
bid1: 12.69,
|
||||
ask1: 12.71,
|
||||
prev_close: 12.00,
|
||||
volume: 100_000,
|
||||
minute_volume: 5_000,
|
||||
bid1_volume: 2_500,
|
||||
ask1_volume: 2_500,
|
||||
trading_phase: Some("open_auction".to_string()),
|
||||
paused: false,
|
||||
upper_limit: 13.20,
|
||||
lower_limit: 10.80,
|
||||
price_tick: 0.01,
|
||||
},
|
||||
DailyMarketSnapshot {
|
||||
date: execution_date,
|
||||
symbol: "000333.SZ".to_string(),
|
||||
timestamp: Some("2023-01-04 09:30:00".to_string()),
|
||||
day_open: 55.60,
|
||||
open: 55.50,
|
||||
high: 56.20,
|
||||
low: 55.00,
|
||||
close: 56.00,
|
||||
last_price: 56.00,
|
||||
bid1: 55.99,
|
||||
ask1: 56.01,
|
||||
prev_close: 54.90,
|
||||
volume: 100_000,
|
||||
minute_volume: 5_000,
|
||||
bid1_volume: 2_500,
|
||||
ask1_volume: 2_500,
|
||||
trading_phase: Some("open_auction".to_string()),
|
||||
paused: false,
|
||||
upper_limit: 60.39,
|
||||
lower_limit: 49.41,
|
||||
price_tick: 0.01,
|
||||
},
|
||||
],
|
||||
vec![],
|
||||
vec![],
|
||||
@@ -27131,8 +27322,8 @@ mod tests {
|
||||
"}"
|
||||
)
|
||||
.to_string(),
|
||||
order_prices_expr: None,
|
||||
valuation_prices_expr: None,
|
||||
order_prices_expr: Some("execution_day_open".to_string()),
|
||||
valuation_prices_expr: Some("execution_day_open".to_string()),
|
||||
when_expr: Some(
|
||||
"decision_date == \"2023-01-03\" && execution_date == \"2023-01-04\""
|
||||
.to_string(),
|
||||
@@ -27145,9 +27336,27 @@ mod tests {
|
||||
|
||||
assert_eq!(decision.order_intents.len(), 1);
|
||||
match &decision.order_intents[0] {
|
||||
crate::strategy::OrderIntent::TargetPortfolioSmart { target_weights, .. } => {
|
||||
crate::strategy::OrderIntent::TargetPortfolioSmart {
|
||||
target_weights,
|
||||
order_prices,
|
||||
valuation_prices,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(target_weights.get("000521.SZ").copied(), Some(0.025));
|
||||
assert_eq!(target_weights.get("000333.SZ").copied(), Some(0.05));
|
||||
match order_prices {
|
||||
Some(TargetPortfolioOrderPricing::LimitPrices(prices)) => {
|
||||
assert_eq!(prices.get("000521.SZ").copied(), Some(12.34));
|
||||
assert_eq!(prices.get("000333.SZ").copied(), Some(55.60));
|
||||
}
|
||||
other => panic!("unexpected order pricing: {other:?}"),
|
||||
}
|
||||
assert_eq!(
|
||||
valuation_prices
|
||||
.as_ref()
|
||||
.and_then(|prices| prices.get("000521.SZ").copied()),
|
||||
Some(12.34)
|
||||
);
|
||||
}
|
||||
other => panic!("unexpected explicit target portfolio intent: {other:?}"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user