支持执行日行情价格映射表达式
This commit is contained in:
@@ -884,6 +884,32 @@ impl PlatformExprStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_reserved_scope_name(name: &str) -> bool {
|
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!(
|
matches!(
|
||||||
name,
|
name,
|
||||||
"signal_close"
|
"signal_close"
|
||||||
@@ -3166,6 +3192,7 @@ impl PlatformExprStrategy {
|
|||||||
day: &DayExpressionState,
|
day: &DayExpressionState,
|
||||||
stock: Option<&StockExpressionState>,
|
stock: Option<&StockExpressionState>,
|
||||||
position: Option<&PositionExpressionState>,
|
position: Option<&PositionExpressionState>,
|
||||||
|
identifiers: &BTreeSet<String>,
|
||||||
include_day_factors: bool,
|
include_day_factors: bool,
|
||||||
include_factors_map: bool,
|
include_factors_map: bool,
|
||||||
include_process_event_counts: bool,
|
include_process_event_counts: bool,
|
||||||
@@ -3183,6 +3210,41 @@ impl PlatformExprStrategy {
|
|||||||
scope.push("signal_close", day.signal_close);
|
scope.push("signal_close", day.signal_close);
|
||||||
scope.push("benchmark_open", day.benchmark_open);
|
scope.push("benchmark_open", day.benchmark_open);
|
||||||
scope.push("benchmark_close", day.benchmark_close);
|
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_ma5", day.signal_ma5);
|
||||||
scope.push("signal_ma10", day.signal_ma10);
|
scope.push("signal_ma10", day.signal_ma10);
|
||||||
scope.push("signal_ma20", day.signal_ma20);
|
scope.push("signal_ma20", day.signal_ma20);
|
||||||
@@ -3823,6 +3885,7 @@ impl PlatformExprStrategy {
|
|||||||
day,
|
day,
|
||||||
stock,
|
stock,
|
||||||
position,
|
position,
|
||||||
|
&normalized_identifiers,
|
||||||
include_day_factors,
|
include_day_factors,
|
||||||
include_factors_map,
|
include_factors_map,
|
||||||
include_process_event_counts,
|
include_process_event_counts,
|
||||||
@@ -5670,14 +5733,15 @@ impl PlatformExprStrategy {
|
|||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
return Ok(BTreeMap::new());
|
return Ok(BTreeMap::new());
|
||||||
}
|
}
|
||||||
let inner = trimmed
|
let Some(inner) = trimmed
|
||||||
.strip_prefix('{')
|
.strip_prefix('{')
|
||||||
.and_then(|value| value.strip_suffix('}'))
|
.and_then(|value| value.strip_suffix('}'))
|
||||||
.ok_or_else(|| {
|
else {
|
||||||
BacktestError::Execution(format!(
|
return Self::dynamic_to_float_map(
|
||||||
"platform float map expr must use {{...}} object literal syntax: {trimmed}"
|
self.eval_dynamic(ctx, trimmed, day, stock, position)?,
|
||||||
))
|
trimmed,
|
||||||
})?;
|
);
|
||||||
|
};
|
||||||
let mut output = BTreeMap::new();
|
let mut output = BTreeMap::new();
|
||||||
for entry in Self::split_top_level_args(inner) {
|
for entry in Self::split_top_level_args(inner) {
|
||||||
let Some((raw_key, raw_value)) = Self::split_top_level_key_value(&entry) else {
|
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)?,
|
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)
|
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(
|
fn action_stock_state(
|
||||||
&self,
|
&self,
|
||||||
ctx: &StrategyContext<'_>,
|
ctx: &StrategyContext<'_>,
|
||||||
@@ -27077,6 +27222,52 @@ mod tests {
|
|||||||
lower_limit: 900.9,
|
lower_limit: 900.9,
|
||||||
price_tick: 0.01,
|
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![],
|
||||||
vec![],
|
vec![],
|
||||||
@@ -27131,8 +27322,8 @@ mod tests {
|
|||||||
"}"
|
"}"
|
||||||
)
|
)
|
||||||
.to_string(),
|
.to_string(),
|
||||||
order_prices_expr: None,
|
order_prices_expr: Some("execution_day_open".to_string()),
|
||||||
valuation_prices_expr: None,
|
valuation_prices_expr: Some("execution_day_open".to_string()),
|
||||||
when_expr: Some(
|
when_expr: Some(
|
||||||
"decision_date == \"2023-01-03\" && execution_date == \"2023-01-04\""
|
"decision_date == \"2023-01-03\" && execution_date == \"2023-01-04\""
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -27145,9 +27336,27 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(decision.order_intents.len(), 1);
|
assert_eq!(decision.order_intents.len(), 1);
|
||||||
match &decision.order_intents[0] {
|
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("000521.SZ").copied(), Some(0.025));
|
||||||
assert_eq!(target_weights.get("000333.SZ").copied(), Some(0.05));
|
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:?}"),
|
other => panic!("unexpected explicit target portfolio intent: {other:?}"),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user