Compare commits
217 Commits
v2026.6.21.3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c094e78bef | |||
| 57345e8230 | |||
| d5d67102ac | |||
| 30a4071ee0 | |||
| 942ba84ca5 | |||
| ab3c821e59 | |||
| 1953e92b7b | |||
| 9cc625409f | |||
| 558d92fe23 | |||
| 0aef8f9491 | |||
| e275f4632d | |||
| 5166916926 | |||
| 56859dbe32 | |||
| 1272e427a1 | |||
| e396c895dc | |||
| f7d0889bbc | |||
| 9b84f3a1b9 | |||
| b1520fcca0 | |||
| bb51d91b76 | |||
| 2c43feec3e | |||
| 825de1d886 | |||
| 7397a2d69f | |||
| 7951ba67e3 | |||
| 2fcacb4313 | |||
| 5e480cd69b | |||
| bfbbac8952 | |||
| bb04864436 | |||
| b87e1b4a02 | |||
| 185ed49fe2 | |||
| a30face86a | |||
| 188376b75a | |||
| 6a98d9b0bd | |||
| a562a8e2ed | |||
| 215c4046d1 | |||
| d30c93989c | |||
| 4554f92fb4 | |||
| 556ed9b848 | |||
| 344e7e90c2 | |||
| c64bf16c8b | |||
| ce5ef3b77d | |||
| 8f47ee3679 | |||
| f15f229a09 | |||
| da12cdddd4 | |||
| 0bb47812e5 | |||
| 203e17ce87 | |||
| 32a417d6d1 | |||
| 2de7127f02 | |||
| a5a9688599 | |||
| ad49fc89d3 | |||
| 6329a8a0da | |||
| 69b793cbb8 | |||
| f8f01a0987 | |||
| 4ce52a7af6 | |||
| ca0471799b | |||
| 0febd3d644 | |||
| c085730ca5 | |||
| 538edb907d | |||
| a854a4ec02 | |||
| ec098c6d39 | |||
| 17bac07a86 | |||
| 63da6bb1bd | |||
| e847ecd54c | |||
| 51ee4a3f54 | |||
| 8c4156948e | |||
| 89f8bb32d0 | |||
| e0a7eb8972 | |||
| 12da5a4704 | |||
| 749b5e3b9c | |||
| e74e2226d5 | |||
| 5a1534e51e | |||
| 22451300b1 | |||
| a50e59ab1d | |||
| 6d86eab021 | |||
| 6b306eecf2 | |||
| 90857fae0a | |||
| b37ebb81f1 | |||
| e37b8e1265 | |||
| 3ef4029c4a | |||
| cb189e3de4 | |||
| 4fee8e1d07 | |||
| 64298f09c1 | |||
| 60bfa28ef0 | |||
| 8c5a2ef611 | |||
| db1ffb5918 | |||
| fe39a75e6e | |||
| 1d33b29c27 | |||
| 831edfc8c6 | |||
| c3ab279d7d | |||
| 6820b63d56 | |||
| fdc099c960 | |||
| afb531da59 | |||
| 7c867f1788 | |||
| a3415095a7 | |||
| 3e3bebf3e0 | |||
| 99a21324db | |||
| 92d5801f63 | |||
| 551421818b | |||
| de1373deae | |||
| f9f9706900 | |||
| 74105f0fde | |||
| 72c667d790 | |||
| 82ed5b3e0b | |||
| b489abba6b | |||
| 203a20592a | |||
| 7059d3a8d0 | |||
| 289448d196 | |||
| c557656040 | |||
| 7189998699 | |||
| 00c9042c15 | |||
| d3c986e1f2 | |||
| 2f61bd8e57 | |||
| 61ad4119cf | |||
| ab31006d01 | |||
| 584a38c7a7 | |||
| 1d817c7f50 | |||
| b327bb074e | |||
| 8ed2b0df7f | |||
| a21ac83f21 | |||
| 13e15cc7c4 | |||
| ad6e168303 | |||
| 73627b1b2d | |||
| 1219b42046 | |||
| ba2470aefe | |||
| 8543c3ab6d | |||
| 9a16ceefbb | |||
| 549595c1c6 | |||
| 8125ea2e3b | |||
| 3e4270729b | |||
| 9aa156eb2a | |||
| 4e3ae3b378 | |||
| 339f85c27b | |||
| aff7fa309c | |||
| cb02041b3b | |||
| e8ecc037c9 | |||
| 652531ac63 | |||
| a8ffd36150 | |||
| 143a021067 | |||
| 487e1a38aa | |||
| e70d637ade | |||
| 8fa4ab24fb | |||
| 14810708f0 | |||
| 3e907d8e43 | |||
| bf457d94ce | |||
| e045ca5a49 | |||
| 84a50111c0 | |||
| 3fae717912 | |||
| 995dd96117 | |||
| c911e79d88 | |||
| f507c63069 | |||
| 723d2c8354 | |||
| 9ab813e74d | |||
| 69576f7e5b | |||
| 3a66c90f34 | |||
| 5481db63df | |||
| 5f2697540a | |||
| 8e4b3d15a4 | |||
| cbe135ed0d | |||
| 73dd006bb2 | |||
| fea09ce93c | |||
| 9fa588fef8 | |||
| f45a5fd0a7 | |||
| 25001fd3e4 | |||
| 3d98ec35e7 | |||
| 5bbe8959f4 | |||
| 3bb001c374 | |||
| c32926cc34 | |||
| 54fb92a780 | |||
| 27d6740dc5 | |||
| cab7c605dc | |||
| e77baffa10 | |||
| 179c4eaff5 | |||
| 32b6da5aca | |||
| 564a2fb9b2 | |||
| 25cc643f34 | |||
| daa0a9b4e6 | |||
| 97931c3766 | |||
| f796a85617 | |||
| 50120e0f9b | |||
| 8715a6171a | |||
| baa77c68e0 | |||
| b176d2ff6f | |||
| 7db0e8da1d | |||
| 754fc91376 | |||
| fb9d8f3b9a | |||
| cad8877b7a | |||
| eae82128ee | |||
| 9f188f6313 | |||
| 6b1afc975e | |||
| 49e883827e | |||
| 2900a40b38 | |||
| a59b687b62 | |||
| 8ba4b4d2c1 | |||
| eeaf061932 | |||
| c3101aa995 | |||
| ec3ec7a26f | |||
| 19b7a0c00c | |||
| 9e6eac557f | |||
| 1623994287 | |||
| fbc6da1a8f | |||
| 49981f2f3e | |||
| 4009fe0899 | |||
| dd8783c8c1 | |||
| 9562b8a280 | |||
| e83856baa9 | |||
| bb690e12c2 | |||
| 41237dccfd | |||
| 6067adc120 | |||
| 275dde61ae | |||
| ab36e6b613 | |||
| a131c761e5 | |||
| 380c34aa66 | |||
| 7f40cfdab0 | |||
| 6db480b91d | |||
| 02e2a20aff | |||
| 1bcedcee0f | |||
| ad405d130e | |||
| c83526a6a4 |
Generated
-10
@@ -37,16 +37,6 @@ version = "2.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bt-demo"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"chrono",
|
|
||||||
"fidc-core",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.20.2"
|
version = "3.20.2"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"crates/fidc-core",
|
"crates/fidc-core",
|
||||||
"crates/bt-demo",
|
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## 当前能力
|
## 当前能力
|
||||||
|
|
||||||
- 日频、分钟、tick 级策略生命周期与确定性回放。
|
- 日频和分钟执行价策略生命周期与确定性回放。
|
||||||
- A 股行情、估值、因子、基准、候选资格、涨跌停触达、停牌和 ST 标记。
|
- A 股行情、估值、因子、基准、候选资格、涨跌停触达、停牌和 ST 标记。
|
||||||
- 平台策略 DSL 与 `StrategyContext` 数据 API,不暴露非平台脚本语法。
|
- 平台策略 DSL 与 `StrategyContext` 数据 API,不暴露非平台脚本语法。
|
||||||
- `BacktestConfig` 支持起止日期、初始资金、决策滞后、执行价格字段、基准代码。
|
- `BacktestConfig` 支持起止日期、初始资金、决策滞后、执行价格字段、基准代码。
|
||||||
@@ -22,7 +22,6 @@
|
|||||||
.
|
.
|
||||||
├── Cargo.toml
|
├── Cargo.toml
|
||||||
├── crates
|
├── crates
|
||||||
│ ├── bt-demo
|
|
||||||
│ └── fidc-core
|
│ └── fidc-core
|
||||||
│ └── src
|
│ └── src
|
||||||
│ ├── broker.rs
|
│ ├── broker.rs
|
||||||
@@ -37,7 +36,6 @@
|
|||||||
│ ├── scheduler.rs
|
│ ├── scheduler.rs
|
||||||
│ ├── strategy.rs
|
│ ├── strategy.rs
|
||||||
│ └── strategy_ai.rs
|
│ └── strategy_ai.rs
|
||||||
├── data/demo
|
|
||||||
└── docs
|
└── docs
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -51,7 +49,7 @@
|
|||||||
- `futures`: 期货账户、合约参数、保证金、手续费和多空持仓。
|
- `futures`: 期货账户、合约参数、保证金、手续费和多空持仓。
|
||||||
- `rules`: 中国市场交易规则和风控校验。
|
- `rules`: 中国市场交易规则和风控校验。
|
||||||
- `broker`: 股票撮合、订单簿、滑点、成交量约束、限价和显式订单执行。
|
- `broker`: 股票撮合、订单簿、滑点、成交量约束、限价和显式订单执行。
|
||||||
- `scheduler`: 日、周、月、分钟、tick 调度规则。
|
- `scheduler`: 日、周、月和分钟调度规则。
|
||||||
- `platform_expr_strategy`: 平台 DSL 解析后的表达式策略执行模型。
|
- `platform_expr_strategy`: 平台 DSL 解析后的表达式策略执行模型。
|
||||||
- `strategy`: 策略 trait、内置策略和运行时视图。
|
- `strategy`: 策略 trait、内置策略和运行时视图。
|
||||||
- `strategy_ai`: 策略 AI 手册、提示词生成和数据库字段目录合并。
|
- `strategy_ai`: 策略 AI 手册、提示词生成和数据库字段目录合并。
|
||||||
@@ -96,45 +94,9 @@
|
|||||||
|
|
||||||
## 运行方式
|
## 运行方式
|
||||||
|
|
||||||
默认运行仓库 demo 数据:
|
`fidc-backtest-engine` 不再维护本地 CSV demo、partitioned snapshot 目录或导出融合表作为运行入口。生产和集成回测由 `fidc-backtest-service` runner 创建 `DataSet`,数据来自 Strategy Factory Source Lake 的 Arrow/Parquet、manifest/data_epoch 缓存和运行时逻辑视图。
|
||||||
|
|
||||||
```bash
|
本仓库只保留核心库构建和测试入口:
|
||||||
cargo run --bin bt-demo
|
|
||||||
```
|
|
||||||
|
|
||||||
运行平台内置微盘策略:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
FIDC_BT_STRATEGY=omni-microcap \
|
|
||||||
FIDC_BT_SIGNAL_SYMBOL=000001.SH \
|
|
||||||
cargo run --release --bin bt-demo
|
|
||||||
```
|
|
||||||
|
|
||||||
接入真实分区 snapshot 目录:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
FIDC_BT_DATA_LAYOUT=partitioned \
|
|
||||||
FIDC_BT_DATA_DIR=/path/to/snapshots \
|
|
||||||
FIDC_BT_SIGNAL_SYMBOL=000001.SH \
|
|
||||||
cargo run --bin bt-demo
|
|
||||||
```
|
|
||||||
|
|
||||||
约定目录结构:
|
|
||||||
|
|
||||||
```text
|
|
||||||
snapshots/
|
|
||||||
├── instruments.csv
|
|
||||||
├── benchmark/YYYY/MM/*.csv
|
|
||||||
├── market/YYYY/MM/*.csv
|
|
||||||
├── factors/YYYY/MM/*.csv
|
|
||||||
└── candidates/YYYY/MM/*.csv
|
|
||||||
```
|
|
||||||
|
|
||||||
运行后默认生成:
|
|
||||||
|
|
||||||
- `output/demo/equity_curve.csv`
|
|
||||||
- `output/demo/trades.csv`
|
|
||||||
- `output/demo/holdings_summary.csv`
|
|
||||||
|
|
||||||
## 测试与构建
|
## 测试与构建
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "bt-demo"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
authors.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
chrono = { workspace = true }
|
|
||||||
fidc-core = { path = "../fidc-core" }
|
|
||||||
serde = { workspace = true }
|
|
||||||
serde_json = "1"
|
|
||||||
@@ -1,567 +0,0 @@
|
|||||||
use std::collections::BTreeSet;
|
|
||||||
use std::error::Error;
|
|
||||||
use std::fs;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use chrono::{NaiveDate, NaiveTime};
|
|
||||||
use fidc_core::{
|
|
||||||
BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, ChinaAShareCostModel,
|
|
||||||
ChinaEquityRuleHooks, CnSmallCapRotationConfig, CnSmallCapRotationStrategy, DailyEquityPoint,
|
|
||||||
DataSet, FillEvent, HoldingSummary, OmniMicroCapConfig, OmniMicroCapStrategy, PortfolioState,
|
|
||||||
PriceField, Strategy, StrategyContext,
|
|
||||||
};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
|
||||||
let root = workspace_root();
|
|
||||||
let data_dir = std::env::var("FIDC_BT_DATA_DIR")
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.unwrap_or_else(|_| root.join("data/demo"));
|
|
||||||
let data_layout = std::env::var("FIDC_BT_DATA_LAYOUT").unwrap_or_else(|_| "flat".to_string());
|
|
||||||
let output_dir = std::env::var("FIDC_BT_OUTPUT_DIR")
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.unwrap_or_else(|_| root.join("output/demo"));
|
|
||||||
let json_output = std::env::var("FIDC_BT_JSON")
|
|
||||||
.map(|value| value == "1" || value.eq_ignore_ascii_case("true"))
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
fs::create_dir_all(&output_dir)?;
|
|
||||||
|
|
||||||
let data = if data_layout == "partitioned" {
|
|
||||||
DataSet::from_partitioned_dir(&data_dir)?
|
|
||||||
} else {
|
|
||||||
DataSet::from_csv_dir(&data_dir)?
|
|
||||||
};
|
|
||||||
let strategy_name =
|
|
||||||
std::env::var("FIDC_BT_STRATEGY").unwrap_or_else(|_| "cn-smallcap-rotation".to_string());
|
|
||||||
let debug_date = std::env::var("FIDC_BT_DEBUG_DATE")
|
|
||||||
.ok()
|
|
||||||
.filter(|value| !value.trim().is_empty())
|
|
||||||
.map(|value| NaiveDate::parse_from_str(value.trim(), "%Y-%m-%d"))
|
|
||||||
.transpose()?;
|
|
||||||
let decision_lag = std::env::var("FIDC_BT_DECISION_LAG")
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| value.parse::<usize>().ok());
|
|
||||||
let execution_price =
|
|
||||||
std::env::var("FIDC_BT_EXECUTION_PRICE")
|
|
||||||
.ok()
|
|
||||||
.map(|value| match value.as_str() {
|
|
||||||
"close" => PriceField::Close,
|
|
||||||
"last" => PriceField::Last,
|
|
||||||
_ => PriceField::Open,
|
|
||||||
});
|
|
||||||
let initial_cash = std::env::var("FIDC_BT_INITIAL_CASH")
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| value.parse::<f64>().ok());
|
|
||||||
let start_date = std::env::var("FIDC_BT_START_DATE")
|
|
||||||
.ok()
|
|
||||||
.filter(|value| !value.trim().is_empty())
|
|
||||||
.map(|value| NaiveDate::parse_from_str(value.trim(), "%Y-%m-%d"))
|
|
||||||
.transpose()?;
|
|
||||||
let end_date = std::env::var("FIDC_BT_END_DATE")
|
|
||||||
.ok()
|
|
||||||
.filter(|value| !value.trim().is_empty())
|
|
||||||
.map(|value| NaiveDate::parse_from_str(value.trim(), "%Y-%m-%d"))
|
|
||||||
.transpose()?;
|
|
||||||
let mut config = BacktestConfig {
|
|
||||||
initial_cash: initial_cash.unwrap_or(1_000_000.0),
|
|
||||||
benchmark_code: data.benchmark_code().to_string(),
|
|
||||||
start_date,
|
|
||||||
end_date,
|
|
||||||
decision_lag_trading_days: 1,
|
|
||||||
execution_price_field: PriceField::Open,
|
|
||||||
};
|
|
||||||
let result = match strategy_name.as_str() {
|
|
||||||
"cn-smallcap-rotation" | "cn-dyn-smallcap-band" => {
|
|
||||||
let mut strategy_cfg = if strategy_name == "cn-dyn-smallcap-band" {
|
|
||||||
CnSmallCapRotationConfig::cn_dyn_smallcap_band()
|
|
||||||
} else {
|
|
||||||
CnSmallCapRotationConfig::demo()
|
|
||||||
};
|
|
||||||
if strategy_cfg.strategy_name == "cn-smallcap-rotation" {
|
|
||||||
strategy_cfg.base_index_level = 3000.0;
|
|
||||||
strategy_cfg.base_cap_floor = 38.0;
|
|
||||||
strategy_cfg.cap_span = 25.0;
|
|
||||||
}
|
|
||||||
if let Ok(signal_symbol) = std::env::var("FIDC_BT_SIGNAL_SYMBOL") {
|
|
||||||
if !signal_symbol.trim().is_empty() {
|
|
||||||
strategy_cfg.signal_symbol = Some(signal_symbol);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
config.decision_lag_trading_days = decision_lag.unwrap_or(1);
|
|
||||||
config.execution_price_field = execution_price.unwrap_or(PriceField::Open);
|
|
||||||
let strategy = CnSmallCapRotationStrategy::new(strategy_cfg);
|
|
||||||
let broker = BrokerSimulator::new_with_execution_price(
|
|
||||||
ChinaAShareCostModel::default(),
|
|
||||||
ChinaEquityRuleHooks::default(),
|
|
||||||
config.execution_price_field,
|
|
||||||
);
|
|
||||||
let mut engine = BacktestEngine::new(data, strategy, broker, config);
|
|
||||||
engine.run()?
|
|
||||||
}
|
|
||||||
"aiquant-v104" => {
|
|
||||||
let mut strategy_cfg = OmniMicroCapConfig::aiquant_v104();
|
|
||||||
if let Ok(signal_symbol) = std::env::var("FIDC_BT_SIGNAL_SYMBOL") {
|
|
||||||
if !signal_symbol.trim().is_empty() {
|
|
||||||
strategy_cfg.benchmark_signal_symbol = signal_symbol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(date) = debug_date {
|
|
||||||
let eligible = data.eligible_universe_on(date);
|
|
||||||
eprintln!(
|
|
||||||
"DEBUG eligible_universe_on {} count={}",
|
|
||||||
date,
|
|
||||||
eligible.len()
|
|
||||||
);
|
|
||||||
for row in eligible.iter().take(20) {
|
|
||||||
eprintln!(" {} {:.6}", row.symbol, row.market_cap_bn);
|
|
||||||
}
|
|
||||||
let mut debug_strategy = OmniMicroCapStrategy::new(strategy_cfg.clone());
|
|
||||||
let debug_subscriptions = BTreeSet::new();
|
|
||||||
let decision = debug_strategy.on_day(&StrategyContext {
|
|
||||||
execution_date: date,
|
|
||||||
decision_date: date,
|
|
||||||
decision_index: 1,
|
|
||||||
data: &data,
|
|
||||||
portfolio: &PortfolioState::new(20_000.0),
|
|
||||||
futures_account: None,
|
|
||||||
open_orders: &[],
|
|
||||||
dynamic_universe: None,
|
|
||||||
subscriptions: &debug_subscriptions,
|
|
||||||
process_events: &[],
|
|
||||||
active_process_event: None,
|
|
||||||
active_datetime: None,
|
|
||||||
order_events: &[],
|
|
||||||
fills: &[],
|
|
||||||
})?;
|
|
||||||
eprintln!("DEBUG notes={:?}", decision.notes);
|
|
||||||
eprintln!("DEBUG diagnostics={:?}", decision.diagnostics);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
config.decision_lag_trading_days = decision_lag.unwrap_or(1);
|
|
||||||
config.execution_price_field = execution_price.unwrap_or(PriceField::Close);
|
|
||||||
config.initial_cash = initial_cash.unwrap_or(20_000.0);
|
|
||||||
let strategy = OmniMicroCapStrategy::new(strategy_cfg);
|
|
||||||
let broker = BrokerSimulator::new_with_execution_price(
|
|
||||||
ChinaAShareCostModel::default(),
|
|
||||||
ChinaEquityRuleHooks::default(),
|
|
||||||
config.execution_price_field,
|
|
||||||
);
|
|
||||||
let mut engine = BacktestEngine::new(data, strategy, broker, config);
|
|
||||||
engine.run()?
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let mut strategy_cfg = OmniMicroCapConfig::omni_microcap();
|
|
||||||
if let Ok(signal_symbol) = std::env::var("FIDC_BT_SIGNAL_SYMBOL") {
|
|
||||||
if !signal_symbol.trim().is_empty() {
|
|
||||||
strategy_cfg.benchmark_signal_symbol = signal_symbol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(date) = debug_date {
|
|
||||||
let eligible = data.eligible_universe_on(date);
|
|
||||||
eprintln!(
|
|
||||||
"DEBUG eligible_universe_on {} count={}",
|
|
||||||
date,
|
|
||||||
eligible.len()
|
|
||||||
);
|
|
||||||
for row in eligible.iter().take(20) {
|
|
||||||
eprintln!(" {} {:.6}", row.symbol, row.market_cap_bn);
|
|
||||||
}
|
|
||||||
let mut debug_strategy = OmniMicroCapStrategy::new(strategy_cfg.clone());
|
|
||||||
let debug_subscriptions = BTreeSet::new();
|
|
||||||
let decision = debug_strategy.on_day(&StrategyContext {
|
|
||||||
execution_date: date,
|
|
||||||
decision_date: date,
|
|
||||||
decision_index: 1,
|
|
||||||
data: &data,
|
|
||||||
portfolio: &PortfolioState::new(10_000_000.0),
|
|
||||||
futures_account: None,
|
|
||||||
open_orders: &[],
|
|
||||||
dynamic_universe: None,
|
|
||||||
subscriptions: &debug_subscriptions,
|
|
||||||
process_events: &[],
|
|
||||||
active_process_event: None,
|
|
||||||
active_datetime: None,
|
|
||||||
order_events: &[],
|
|
||||||
fills: &[],
|
|
||||||
})?;
|
|
||||||
eprintln!("DEBUG notes={:?}", decision.notes);
|
|
||||||
eprintln!("DEBUG diagnostics={:?}", decision.diagnostics);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
config.decision_lag_trading_days = decision_lag.unwrap_or(0);
|
|
||||||
config.execution_price_field = execution_price.unwrap_or(PriceField::Last);
|
|
||||||
config.initial_cash = initial_cash.unwrap_or(10_000_000.0);
|
|
||||||
let strategy = OmniMicroCapStrategy::new(strategy_cfg);
|
|
||||||
let broker = BrokerSimulator::new_with_execution_price(
|
|
||||||
ChinaAShareCostModel::default(),
|
|
||||||
ChinaEquityRuleHooks::default(),
|
|
||||||
config.execution_price_field,
|
|
||||||
)
|
|
||||||
.with_intraday_execution_start_time(
|
|
||||||
NaiveTime::parse_from_str("10:18:00", "%H:%M:%S").expect("valid 10:18:00"),
|
|
||||||
)
|
|
||||||
.with_volume_limit(false)
|
|
||||||
.with_inactive_limit(false)
|
|
||||||
.with_liquidity_limit(false);
|
|
||||||
let mut engine = BacktestEngine::new(data, strategy, broker, config);
|
|
||||||
engine.run()?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
write_equity_curve_csv(&output_dir.join("equity_curve.csv"), &result.equity_curve)?;
|
|
||||||
write_trades_csv(&output_dir.join("trades.csv"), &result.fills)?;
|
|
||||||
write_holdings_csv(
|
|
||||||
&output_dir.join("holdings_summary.csv"),
|
|
||||||
&result.holdings_summary,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let summary = build_summary(
|
|
||||||
&result.strategy_name,
|
|
||||||
&result.equity_curve,
|
|
||||||
&result.fills,
|
|
||||||
&result.holdings_summary,
|
|
||||||
result.benchmark_series.last(),
|
|
||||||
&output_dir,
|
|
||||||
);
|
|
||||||
|
|
||||||
print_summary(&summary, &result.equity_curve, &result.holdings_summary);
|
|
||||||
println!("Artifacts written under {}", output_dir.display());
|
|
||||||
if json_output {
|
|
||||||
println!("{}", serde_json::to_string(&summary)?);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn workspace_root() -> PathBuf {
|
|
||||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
||||||
.join("../..")
|
|
||||||
.canonicalize()
|
|
||||||
.expect("workspace root")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_equity_curve_csv(path: &Path, rows: &[DailyEquityPoint]) -> Result<(), Box<dyn Error>> {
|
|
||||||
let mut file = fs::File::create(path)?;
|
|
||||||
writeln!(
|
|
||||||
file,
|
|
||||||
"date,cash,market_value,total_equity,benchmark_close,benchmark_prev_close,notes,diagnostics"
|
|
||||||
)?;
|
|
||||||
for row in rows {
|
|
||||||
writeln!(
|
|
||||||
file,
|
|
||||||
"{},{:.2},{:.2},{:.2},{:.2},{:.2},{},{}",
|
|
||||||
row.date,
|
|
||||||
row.cash,
|
|
||||||
row.market_value,
|
|
||||||
row.total_equity,
|
|
||||||
row.benchmark_close,
|
|
||||||
row.benchmark_prev_close,
|
|
||||||
sanitize_csv_field(&row.notes),
|
|
||||||
sanitize_csv_field(&row.diagnostics),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_trades_csv(path: &Path, rows: &[FillEvent]) -> Result<(), Box<dyn Error>> {
|
|
||||||
let mut file = fs::File::create(path)?;
|
|
||||||
writeln!(
|
|
||||||
file,
|
|
||||||
"date,symbol,side,quantity,price,gross_amount,commission,stamp_tax,net_cash_flow,reason"
|
|
||||||
)?;
|
|
||||||
for row in rows {
|
|
||||||
writeln!(
|
|
||||||
file,
|
|
||||||
"{},{},{:?},{},{:.2},{:.2},{:.2},{:.2},{:.2},{}",
|
|
||||||
row.date,
|
|
||||||
row.symbol,
|
|
||||||
row.side,
|
|
||||||
row.quantity,
|
|
||||||
row.price,
|
|
||||||
row.gross_amount,
|
|
||||||
row.commission,
|
|
||||||
row.stamp_tax,
|
|
||||||
row.net_cash_flow,
|
|
||||||
sanitize_csv_field(&row.reason),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_holdings_csv(path: &Path, rows: &[HoldingSummary]) -> Result<(), Box<dyn Error>> {
|
|
||||||
let mut file = fs::File::create(path)?;
|
|
||||||
writeln!(
|
|
||||||
file,
|
|
||||||
"date,symbol,quantity,average_cost,last_price,market_value,unrealized_pnl,realized_pnl"
|
|
||||||
)?;
|
|
||||||
for row in rows {
|
|
||||||
writeln!(
|
|
||||||
file,
|
|
||||||
"{},{},{},{:.2},{:.2},{:.2},{:.2},{:.2}",
|
|
||||||
row.date,
|
|
||||||
row.symbol,
|
|
||||||
row.quantity,
|
|
||||||
row.average_cost,
|
|
||||||
row.last_price,
|
|
||||||
row.market_value,
|
|
||||||
row.unrealized_pnl,
|
|
||||||
row.realized_pnl,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sanitize_csv_field(text: &str) -> String {
|
|
||||||
text.replace(',', ";")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize)]
|
|
||||||
struct RunSummary {
|
|
||||||
strategy: String,
|
|
||||||
start_date: String,
|
|
||||||
end_date: String,
|
|
||||||
start_equity: f64,
|
|
||||||
final_equity: f64,
|
|
||||||
total_return: f64,
|
|
||||||
trade_count: usize,
|
|
||||||
holding_count: usize,
|
|
||||||
benchmark_code: Option<String>,
|
|
||||||
benchmark_last_close: Option<f64>,
|
|
||||||
output_dir: String,
|
|
||||||
diagnostics: serde_json::Value,
|
|
||||||
warnings: Vec<String>,
|
|
||||||
equity_preview: Vec<serde_json::Value>,
|
|
||||||
trades_preview: Vec<serde_json::Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_summary(
|
|
||||||
strategy_name: &str,
|
|
||||||
equity_curve: &[DailyEquityPoint],
|
|
||||||
fills: &[FillEvent],
|
|
||||||
holdings: &[HoldingSummary],
|
|
||||||
benchmark_last: Option<&BenchmarkSnapshot>,
|
|
||||||
output_dir: &Path,
|
|
||||||
) -> RunSummary {
|
|
||||||
let first = equity_curve.first();
|
|
||||||
let last = equity_curve.last();
|
|
||||||
let start_equity = first.map(|row| row.total_equity).unwrap_or_default();
|
|
||||||
let final_equity = last.map(|row| row.total_equity).unwrap_or_default();
|
|
||||||
let total_return = if start_equity.abs() < f64::EPSILON {
|
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
(final_equity / start_equity) - 1.0
|
|
||||||
};
|
|
||||||
|
|
||||||
let diagnostics = extract_diagnostics(equity_curve);
|
|
||||||
let warnings = build_warnings(fills, holdings, &diagnostics);
|
|
||||||
let equity_preview = equity_curve
|
|
||||||
.iter()
|
|
||||||
.rev()
|
|
||||||
.take(5)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.into_iter()
|
|
||||||
.rev()
|
|
||||||
.map(|row| {
|
|
||||||
json!({
|
|
||||||
"date": row.date.to_string(),
|
|
||||||
"cash": row.cash,
|
|
||||||
"marketValue": row.market_value,
|
|
||||||
"totalEquity": row.total_equity,
|
|
||||||
"benchmarkClose": row.benchmark_close,
|
|
||||||
"benchmarkPrevClose": row.benchmark_prev_close,
|
|
||||||
"notes": row.notes,
|
|
||||||
"diagnostics": row.diagnostics,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let trades_preview = fills
|
|
||||||
.iter()
|
|
||||||
.rev()
|
|
||||||
.take(10)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.into_iter()
|
|
||||||
.rev()
|
|
||||||
.map(|row| {
|
|
||||||
json!({
|
|
||||||
"date": row.date.to_string(),
|
|
||||||
"symbol": row.symbol,
|
|
||||||
"side": format!("{:?}", row.side),
|
|
||||||
"quantity": row.quantity,
|
|
||||||
"price": row.price,
|
|
||||||
"grossAmount": row.gross_amount,
|
|
||||||
"netCashFlow": row.net_cash_flow,
|
|
||||||
"reason": row.reason,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
RunSummary {
|
|
||||||
strategy: strategy_name.to_string(),
|
|
||||||
start_date: first.map(|row| row.date.to_string()).unwrap_or_default(),
|
|
||||||
end_date: last.map(|row| row.date.to_string()).unwrap_or_default(),
|
|
||||||
start_equity,
|
|
||||||
final_equity,
|
|
||||||
total_return,
|
|
||||||
trade_count: fills.len(),
|
|
||||||
holding_count: holdings.len(),
|
|
||||||
benchmark_code: benchmark_last.map(|row| row.benchmark.clone()),
|
|
||||||
benchmark_last_close: benchmark_last.map(|row| row.close),
|
|
||||||
output_dir: output_dir.display().to_string(),
|
|
||||||
diagnostics,
|
|
||||||
warnings,
|
|
||||||
equity_preview,
|
|
||||||
trades_preview,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_diagnostics(equity_curve: &[DailyEquityPoint]) -> serde_json::Value {
|
|
||||||
let last = equity_curve.last();
|
|
||||||
let text = last.map(|row| row.diagnostics.as_str()).unwrap_or("");
|
|
||||||
let notes = last.map(|row| row.notes.as_str()).unwrap_or("");
|
|
||||||
let mut map = serde_json::Map::new();
|
|
||||||
map.insert("latestText".to_string(), json!(text));
|
|
||||||
map.insert("latestNotes".to_string(), json!(notes));
|
|
||||||
map.insert("equityPointCount".to_string(), json!(equity_curve.len()));
|
|
||||||
|
|
||||||
for part in text.split(" | ") {
|
|
||||||
let part = part.trim();
|
|
||||||
if let Some(rest) = part.strip_prefix("selection_diag ") {
|
|
||||||
for token in rest.split_whitespace() {
|
|
||||||
if let Some((k, v)) = token.split_once('=') {
|
|
||||||
map.insert(k.to_string(), parse_diag_value(v));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if let Some(rest) = part.strip_prefix("selection_band ") {
|
|
||||||
for token in rest.split_whitespace() {
|
|
||||||
if let Some((k, v)) = token.split_once('=') {
|
|
||||||
map.insert(k.to_string(), parse_diag_value(v));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if let Some(rest) =
|
|
||||||
part.strip_prefix("market_cap_missing likely blocks selection; sample=")
|
|
||||||
{
|
|
||||||
map.insert(
|
|
||||||
"marketCapMissingSample".to_string(),
|
|
||||||
json!(
|
|
||||||
rest.split('|')
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if let Some(rest) = part.strip_prefix("selection_rejections sample=") {
|
|
||||||
map.insert(
|
|
||||||
"selectionRejectionsSample".to_string(),
|
|
||||||
json!(
|
|
||||||
rest.split(" | ")
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if let Some(rest) = part.strip_prefix("ma_filter_rejections sample=") {
|
|
||||||
map.insert(
|
|
||||||
"maFilterRejectionsSample".to_string(),
|
|
||||||
json!(
|
|
||||||
rest.split('|')
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if let Some(rest) = part.strip_prefix("selected=") {
|
|
||||||
map.insert("selectedLine".to_string(), json!(rest));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
serde_json::Value::Object(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_diag_value(value: &str) -> serde_json::Value {
|
|
||||||
if let Ok(v) = value.parse::<i64>() {
|
|
||||||
return json!(v);
|
|
||||||
}
|
|
||||||
if let Ok(v) = value.parse::<f64>() {
|
|
||||||
return json!(v);
|
|
||||||
}
|
|
||||||
json!(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_warnings(
|
|
||||||
fills: &[FillEvent],
|
|
||||||
holdings: &[HoldingSummary],
|
|
||||||
diagnostics: &serde_json::Value,
|
|
||||||
) -> Vec<String> {
|
|
||||||
let mut warnings = Vec::new();
|
|
||||||
if fills.is_empty() {
|
|
||||||
warnings.push("本次回测没有产生任何成交。".to_string());
|
|
||||||
}
|
|
||||||
if holdings.is_empty() {
|
|
||||||
warnings.push("期末没有持仓。".to_string());
|
|
||||||
}
|
|
||||||
let selected_after_ma_is_empty = diagnostics
|
|
||||||
.get("selected_after_ma")
|
|
||||||
.and_then(|v| v.as_i64())
|
|
||||||
.unwrap_or(0)
|
|
||||||
== 0;
|
|
||||||
if selected_after_ma_is_empty && fills.is_empty() && holdings.is_empty() {
|
|
||||||
warnings
|
|
||||||
.push("最终没有股票通过完整选股链路,结果为空时请优先查看 diagnostics。".to_string());
|
|
||||||
}
|
|
||||||
if diagnostics
|
|
||||||
.get("market_cap_missing_count")
|
|
||||||
.and_then(|v| v.as_i64())
|
|
||||||
.unwrap_or(0)
|
|
||||||
> 0
|
|
||||||
{
|
|
||||||
warnings.push("存在 market_cap 缺失或非正值,当前会直接阻断该股票进入候选池。".to_string());
|
|
||||||
}
|
|
||||||
warnings
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_summary(
|
|
||||||
summary: &RunSummary,
|
|
||||||
equity_curve: &[DailyEquityPoint],
|
|
||||||
holdings: &[HoldingSummary],
|
|
||||||
) {
|
|
||||||
if equity_curve.is_empty() {
|
|
||||||
println!("No equity curve points generated.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Strategy: {}", summary.strategy);
|
|
||||||
println!("Start equity: {:.2}", summary.start_equity);
|
|
||||||
println!("Final equity: {:.2}", summary.final_equity);
|
|
||||||
println!("Total return: {:.2}%", summary.total_return * 100.0);
|
|
||||||
println!("Trades: {}", summary.trade_count);
|
|
||||||
println!("Final holdings: {}", summary.holding_count);
|
|
||||||
|
|
||||||
if let (Some(code), Some(close)) = (&summary.benchmark_code, summary.benchmark_last_close) {
|
|
||||||
println!("Benchmark last close: {} {:.2}", code, close);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Recent equity points:");
|
|
||||||
for point in equity_curve
|
|
||||||
.iter()
|
|
||||||
.rev()
|
|
||||||
.take(3)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.into_iter()
|
|
||||||
.rev()
|
|
||||||
{
|
|
||||||
println!(
|
|
||||||
" {} equity {:.2} cash {:.2} mv {:.2}",
|
|
||||||
point.date, point.total_equity, point.cash, point.market_value
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if holdings.is_empty() {
|
|
||||||
println!("No holdings at the end of the demo run.");
|
|
||||||
} else {
|
|
||||||
println!("Ending holdings:");
|
|
||||||
for holding in holdings {
|
|
||||||
println!(
|
|
||||||
" {} qty {} mv {:.2} pnl {:.2}",
|
|
||||||
holding.symbol, holding.quantity, holding.market_value, holding.unrealized_pnl
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2650
-130
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ use std::collections::BTreeMap;
|
|||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
use crate::events::OrderSide;
|
use crate::events::OrderSide;
|
||||||
|
use crate::risk_control::TradingConstraintConfig;
|
||||||
|
|
||||||
pub const STOCK_PIT_TAX_CHANGE_DATE: (i32, u32, u32) = (2023, 8, 28);
|
pub const STOCK_PIT_TAX_CHANGE_DATE: (i32, u32, u32) = (2023, 8, 28);
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ pub struct ChinaAShareCostModel {
|
|||||||
pub commission_rate: f64,
|
pub commission_rate: f64,
|
||||||
pub stamp_tax_rate_before_change: f64,
|
pub stamp_tax_rate_before_change: f64,
|
||||||
pub stamp_tax_rate_after_change: f64,
|
pub stamp_tax_rate_after_change: f64,
|
||||||
|
pub stamp_tax_change_date: NaiveDate,
|
||||||
pub minimum_commission: f64,
|
pub minimum_commission: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,20 +49,32 @@ impl Default for ChinaAShareCostModel {
|
|||||||
commission_rate: 0.0008,
|
commission_rate: 0.0008,
|
||||||
stamp_tax_rate_before_change: 0.001,
|
stamp_tax_rate_before_change: 0.001,
|
||||||
stamp_tax_rate_after_change: 0.0005,
|
stamp_tax_rate_after_change: 0.0005,
|
||||||
|
stamp_tax_change_date: default_stamp_tax_change_date(),
|
||||||
minimum_commission: 5.0,
|
minimum_commission: 5.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChinaAShareCostModel {
|
impl ChinaAShareCostModel {
|
||||||
pub fn aiquant_rqalpha_default() -> Self {
|
pub fn aiquant_default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
commission_rate: 0.0003,
|
||||||
stamp_tax_rate_before_change: 0.0005,
|
stamp_tax_rate_before_change: 0.0005,
|
||||||
stamp_tax_rate_after_change: 0.0005,
|
stamp_tax_rate_after_change: 0.0005,
|
||||||
..Self::default()
|
..Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_trading_constraints(config: TradingConstraintConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
commission_rate: config.commission_rate,
|
||||||
|
stamp_tax_rate_before_change: config.stamp_tax_rate_before_change,
|
||||||
|
stamp_tax_rate_after_change: config.stamp_tax_rate_after_change,
|
||||||
|
stamp_tax_change_date: config.stamp_tax_change_date,
|
||||||
|
minimum_commission: config.minimum_commission,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn commission_for(&self, gross_amount: f64) -> f64 {
|
pub fn commission_for(&self, gross_amount: f64) -> f64 {
|
||||||
if gross_amount <= 0.0 {
|
if gross_amount <= 0.0 {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
@@ -69,13 +83,7 @@ impl ChinaAShareCostModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn stamp_tax_rate_for(&self, date: NaiveDate) -> f64 {
|
pub fn stamp_tax_rate_for(&self, date: NaiveDate) -> f64 {
|
||||||
let change_date = NaiveDate::from_ymd_opt(
|
if date < self.stamp_tax_change_date {
|
||||||
STOCK_PIT_TAX_CHANGE_DATE.0,
|
|
||||||
STOCK_PIT_TAX_CHANGE_DATE.1,
|
|
||||||
STOCK_PIT_TAX_CHANGE_DATE.2,
|
|
||||||
)
|
|
||||||
.expect("valid pit tax change date");
|
|
||||||
if date < change_date {
|
|
||||||
self.stamp_tax_rate_before_change
|
self.stamp_tax_rate_before_change
|
||||||
} else {
|
} else {
|
||||||
self.stamp_tax_rate_after_change
|
self.stamp_tax_rate_after_change
|
||||||
@@ -127,6 +135,15 @@ impl ChinaAShareCostModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_stamp_tax_change_date() -> NaiveDate {
|
||||||
|
NaiveDate::from_ymd_opt(
|
||||||
|
STOCK_PIT_TAX_CHANGE_DATE.0,
|
||||||
|
STOCK_PIT_TAX_CHANGE_DATE.1,
|
||||||
|
STOCK_PIT_TAX_CHANGE_DATE.2,
|
||||||
|
)
|
||||||
|
.expect("valid pit tax change date")
|
||||||
|
}
|
||||||
|
|
||||||
impl CostModel for ChinaAShareCostModel {
|
impl CostModel for ChinaAShareCostModel {
|
||||||
fn calculate(&self, date: NaiveDate, side: OrderSide, gross_amount: f64) -> TradingCost {
|
fn calculate(&self, date: NaiveDate, side: OrderSide, gross_amount: f64) -> TradingCost {
|
||||||
if gross_amount <= 0.0 {
|
if gross_amount <= 0.0 {
|
||||||
@@ -169,3 +186,51 @@ impl CostModel for ChinaAShareCostModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aiquant_default_matches_current_backtest_fee_model() {
|
||||||
|
let model = ChinaAShareCostModel::aiquant_default();
|
||||||
|
let date = NaiveDate::from_ymd_opt(2025, 11, 11).expect("valid date");
|
||||||
|
|
||||||
|
assert!((model.commission_for(248_059.812) - 74.4179436).abs() < 1e-9);
|
||||||
|
assert!(
|
||||||
|
(model.stamp_tax_for(date, OrderSide::Sell, 245_747.007) - 122.8735035).abs() < 1e-9
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cost_model_can_use_configurable_stamp_tax_change_date() {
|
||||||
|
let config = TradingConstraintConfig {
|
||||||
|
commission_rate: 0.0003,
|
||||||
|
minimum_commission: 5.0,
|
||||||
|
stamp_tax_rate_before_change: 0.002,
|
||||||
|
stamp_tax_rate_after_change: 0.001,
|
||||||
|
stamp_tax_change_date: NaiveDate::from_ymd_opt(2025, 1, 10).expect("valid date"),
|
||||||
|
..TradingConstraintConfig::default()
|
||||||
|
};
|
||||||
|
let model = ChinaAShareCostModel::from_trading_constraints(config);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
(model.stamp_tax_for(
|
||||||
|
NaiveDate::from_ymd_opt(2025, 1, 9).expect("valid date"),
|
||||||
|
OrderSide::Sell,
|
||||||
|
10_000.0
|
||||||
|
) - 20.0)
|
||||||
|
.abs()
|
||||||
|
< 1e-9
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(model.stamp_tax_for(
|
||||||
|
NaiveDate::from_ymd_opt(2025, 1, 10).expect("valid date"),
|
||||||
|
OrderSide::Sell,
|
||||||
|
10_000.0
|
||||||
|
) - 10.0)
|
||||||
|
.abs()
|
||||||
|
< 1e-9
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+371
-771
File diff suppressed because it is too large
Load Diff
+1909
-96
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,33 @@ mod date_format {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod optional_date_format {
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||||
|
|
||||||
|
const FORMAT: &str = "%Y-%m-%d";
|
||||||
|
|
||||||
|
pub fn serialize<S>(date: &Option<NaiveDate>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
match date {
|
||||||
|
Some(date) => serializer.serialize_some(&date.format(FORMAT).to_string()),
|
||||||
|
None => serializer.serialize_none(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<NaiveDate>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value = Option::<String>::deserialize(deserializer)?;
|
||||||
|
value
|
||||||
|
.map(|text| NaiveDate::parse_from_str(&text, FORMAT).map_err(serde::de::Error::custom))
|
||||||
|
.transpose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum OrderSide {
|
pub enum OrderSide {
|
||||||
Buy,
|
Buy,
|
||||||
@@ -63,6 +90,12 @@ impl OrderStatus {
|
|||||||
pub struct OrderEvent {
|
pub struct OrderEvent {
|
||||||
#[serde(with = "date_format")]
|
#[serde(with = "date_format")]
|
||||||
pub date: NaiveDate,
|
pub date: NaiveDate,
|
||||||
|
#[serde(default, with = "optional_date_format")]
|
||||||
|
pub decision_date: Option<NaiveDate>,
|
||||||
|
#[serde(default, with = "optional_date_format")]
|
||||||
|
pub order_created_date: Option<NaiveDate>,
|
||||||
|
#[serde(default, with = "optional_date_format")]
|
||||||
|
pub execution_date: Option<NaiveDate>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub order_id: Option<u64>,
|
pub order_id: Option<u64>,
|
||||||
pub symbol: String,
|
pub symbol: String,
|
||||||
@@ -77,6 +110,12 @@ pub struct OrderEvent {
|
|||||||
pub struct FillEvent {
|
pub struct FillEvent {
|
||||||
#[serde(with = "date_format")]
|
#[serde(with = "date_format")]
|
||||||
pub date: NaiveDate,
|
pub date: NaiveDate,
|
||||||
|
#[serde(default, with = "optional_date_format")]
|
||||||
|
pub decision_date: Option<NaiveDate>,
|
||||||
|
#[serde(default, with = "optional_date_format")]
|
||||||
|
pub order_created_date: Option<NaiveDate>,
|
||||||
|
#[serde(default, with = "optional_date_format")]
|
||||||
|
pub execution_date: Option<NaiveDate>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub order_id: Option<u64>,
|
pub order_id: Option<u64>,
|
||||||
pub symbol: String,
|
pub symbol: String,
|
||||||
@@ -123,9 +162,9 @@ pub enum ProcessEventKind {
|
|||||||
PreBar,
|
PreBar,
|
||||||
Bar,
|
Bar,
|
||||||
PostBar,
|
PostBar,
|
||||||
PreTick,
|
PreMinute,
|
||||||
Tick,
|
Minute,
|
||||||
PostTick,
|
PostMinute,
|
||||||
PreScheduled,
|
PreScheduled,
|
||||||
PostScheduled,
|
PostScheduled,
|
||||||
PreOnDay,
|
PreOnDay,
|
||||||
@@ -165,9 +204,9 @@ impl ProcessEventKind {
|
|||||||
Self::PreBar => "pre_bar",
|
Self::PreBar => "pre_bar",
|
||||||
Self::Bar => "bar",
|
Self::Bar => "bar",
|
||||||
Self::PostBar => "post_bar",
|
Self::PostBar => "post_bar",
|
||||||
Self::PreTick => "pre_tick",
|
Self::PreMinute => "pre_minute",
|
||||||
Self::Tick => "tick",
|
Self::Minute => "minute",
|
||||||
Self::PostTick => "post_tick",
|
Self::PostMinute => "post_minute",
|
||||||
Self::PreScheduled => "pre_scheduled",
|
Self::PreScheduled => "pre_scheduled",
|
||||||
Self::PostScheduled => "post_scheduled",
|
Self::PostScheduled => "post_scheduled",
|
||||||
Self::PreOnDay => "pre_on_day",
|
Self::PreOnDay => "pre_on_day",
|
||||||
|
|||||||
@@ -746,6 +746,9 @@ impl FuturesAccountState {
|
|||||||
);
|
);
|
||||||
report.order_events.push(OrderEvent {
|
report.order_events.push(OrderEvent {
|
||||||
date,
|
date,
|
||||||
|
decision_date: None,
|
||||||
|
order_created_date: None,
|
||||||
|
execution_date: None,
|
||||||
order_id,
|
order_id,
|
||||||
symbol: intent.symbol,
|
symbol: intent.symbol,
|
||||||
side,
|
side,
|
||||||
@@ -823,6 +826,9 @@ impl FuturesAccountState {
|
|||||||
intent.price * intent.quantity as f64 * intent.spec.contract_multiplier;
|
intent.price * intent.quantity as f64 * intent.spec.contract_multiplier;
|
||||||
report.fill_events.push(FillEvent {
|
report.fill_events.push(FillEvent {
|
||||||
date,
|
date,
|
||||||
|
decision_date: None,
|
||||||
|
order_created_date: None,
|
||||||
|
execution_date: None,
|
||||||
order_id,
|
order_id,
|
||||||
symbol: intent.symbol.clone(),
|
symbol: intent.symbol.clone(),
|
||||||
side,
|
side,
|
||||||
@@ -889,6 +895,9 @@ impl FuturesAccountState {
|
|||||||
});
|
});
|
||||||
report.order_events.push(OrderEvent {
|
report.order_events.push(OrderEvent {
|
||||||
date,
|
date,
|
||||||
|
decision_date: None,
|
||||||
|
order_created_date: None,
|
||||||
|
execution_date: None,
|
||||||
order_id,
|
order_id,
|
||||||
symbol: intent.symbol,
|
symbol: intent.symbol,
|
||||||
side,
|
side,
|
||||||
@@ -915,6 +924,9 @@ impl FuturesAccountState {
|
|||||||
);
|
);
|
||||||
report.order_events.push(OrderEvent {
|
report.order_events.push(OrderEvent {
|
||||||
date,
|
date,
|
||||||
|
decision_date: None,
|
||||||
|
order_created_date: None,
|
||||||
|
execution_date: None,
|
||||||
order_id,
|
order_id,
|
||||||
symbol: intent.symbol,
|
symbol: intent.symbol,
|
||||||
side,
|
side,
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ impl Instrument {
|
|||||||
.is_some_and(|delisted_at| delisted_at < date)
|
.is_some_and(|delisted_at| delisted_at < date)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_delisted_on_or_before(&self, date: NaiveDate) -> bool {
|
||||||
|
self.delisted_at
|
||||||
|
.is_some_and(|delisted_at| delisted_at <= date)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_active_on(&self, date: NaiveDate) -> bool {
|
pub fn is_active_on(&self, date: NaiveDate) -> bool {
|
||||||
self.listed_at.is_none_or(|listed_at| listed_at <= date)
|
self.listed_at.is_none_or(|listed_at| listed_at <= date)
|
||||||
&& !self.is_delisted_before(date)
|
&& !self.is_delisted_before(date)
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ pub mod strategy_ai;
|
|||||||
pub mod universe;
|
pub mod universe;
|
||||||
|
|
||||||
pub use broker::{
|
pub use broker::{
|
||||||
BrokerExecutionReport, BrokerSimulator, DynamicSlippageConfig, MatchingType, SlippageModel,
|
BrokerExecutionReport, BrokerSimulator, DynamicSlippageConfig, MatchingType, RebalanceCashMode,
|
||||||
|
SlippageModel,
|
||||||
};
|
};
|
||||||
pub use calendar::TradingCalendar;
|
pub use calendar::TradingCalendar;
|
||||||
pub use cost::{ChinaAShareCostModel, CostModel, TradingCost};
|
pub use cost::{ChinaAShareCostModel, CostModel, TradingCost};
|
||||||
@@ -69,7 +70,10 @@ pub use platform_strategy_spec::{
|
|||||||
StrategyRuntimeSpec, platform_expr_config_from_spec, platform_expr_config_from_value,
|
StrategyRuntimeSpec, platform_expr_config_from_spec, platform_expr_config_from_value,
|
||||||
};
|
};
|
||||||
pub use portfolio::{CashReceivable, HoldingSummary, PendingCashFlow, PortfolioState, Position};
|
pub use portfolio::{CashReceivable, HoldingSummary, PendingCashFlow, PortfolioState, Position};
|
||||||
pub use risk_control::ChinaAShareRiskControl;
|
pub use risk_control::{
|
||||||
|
ChinaAShareRiskControl, FidcRiskControlConfig, FidcRiskDecisionAudit, RiskCheckScope,
|
||||||
|
StaticRiskRuleConfig, TradingConstraintConfig,
|
||||||
|
};
|
||||||
pub use rules::{ChinaEquityRuleHooks, EquityRuleHooks, RuleCheck};
|
pub use rules::{ChinaEquityRuleHooks, EquityRuleHooks, RuleCheck};
|
||||||
pub use scheduler::{
|
pub use scheduler::{
|
||||||
ScheduleFrequency, ScheduleRule, ScheduleStage, ScheduleTimeRule, Scheduler, default_stage_time,
|
ScheduleFrequency, ScheduleRule, ScheduleStage, ScheduleTimeRule, Scheduler, default_stage_time,
|
||||||
@@ -81,9 +85,10 @@ pub use strategy::{
|
|||||||
};
|
};
|
||||||
pub use strategy_ai::{
|
pub use strategy_ai::{
|
||||||
ManualExample, ManualFactorSource, ManualField, ManualFieldGroup, ManualFunction,
|
ManualExample, ManualFactorSource, ManualField, ManualFieldGroup, ManualFunction,
|
||||||
ManualSection, StrategyAiCatalog, StrategyAiGenerateRequest, StrategyAiManual,
|
ManualSection, StrategyAiCatalog, StrategyAiGenerateRequest, StrategyAiHoldingCountContract,
|
||||||
StrategyAiOptimizeRequest, build_generation_prompt, build_optimization_prompt,
|
StrategyAiManual, StrategyAiOptimizeRequest, build_generation_prompt,
|
||||||
built_in_strategy_manual, merge_catalog_into_manual, render_manual_markdown,
|
build_optimization_prompt, built_in_strategy_manual, merge_catalog_into_manual,
|
||||||
|
render_manual_markdown,
|
||||||
};
|
};
|
||||||
pub use universe::{
|
pub use universe::{
|
||||||
BandRegime, DynamicMarketCapBandSelector, SelectionContext, SelectionDiagnostics,
|
BandRegime, DynamicMarketCapBandSelector, SelectionContext, SelectionDiagnostics,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -135,11 +135,12 @@ const RESERVED_SCOPE_NAMES: &[&str] = &[
|
|||||||
"free_float_cap",
|
"free_float_cap",
|
||||||
"pe_ttm",
|
"pe_ttm",
|
||||||
"volume",
|
"volume",
|
||||||
"tick_volume",
|
"minute_volume",
|
||||||
"bid1_volume",
|
"bid1_volume",
|
||||||
"ask1_volume",
|
"ask1_volume",
|
||||||
"turnover_ratio",
|
"turnover_ratio",
|
||||||
"effective_turnover_ratio",
|
"effective_turnover_ratio",
|
||||||
|
"up_days_stock",
|
||||||
"open",
|
"open",
|
||||||
"high",
|
"high",
|
||||||
"low",
|
"low",
|
||||||
@@ -154,7 +155,9 @@ const RESERVED_SCOPE_NAMES: &[&str] = &[
|
|||||||
"round_lot",
|
"round_lot",
|
||||||
"paused",
|
"paused",
|
||||||
"is_st",
|
"is_st",
|
||||||
|
"is_star_st",
|
||||||
"is_kcb",
|
"is_kcb",
|
||||||
|
"is_bjse",
|
||||||
"is_one_yuan",
|
"is_one_yuan",
|
||||||
"is_new_listing",
|
"is_new_listing",
|
||||||
"allow_buy",
|
"allow_buy",
|
||||||
@@ -327,6 +330,7 @@ mod tests {
|
|||||||
"avg_cost",
|
"avg_cost",
|
||||||
"current_price",
|
"current_price",
|
||||||
"stock_ma_short",
|
"stock_ma_short",
|
||||||
|
"up_days_stock",
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
names.contains(required),
|
names.contains(required),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -82,6 +82,8 @@ impl Position {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let previous_quantity = self.quantity;
|
||||||
|
let previous_average_cost = self.average_cost;
|
||||||
self.lots.push(PositionLot {
|
self.lots.push(PositionLot {
|
||||||
acquired_date: date,
|
acquired_date: date,
|
||||||
quantity,
|
quantity,
|
||||||
@@ -93,7 +95,18 @@ impl Position {
|
|||||||
self.day_trade_quantity_delta += quantity as i32;
|
self.day_trade_quantity_delta += quantity as i32;
|
||||||
self.day_buy_quantity += quantity;
|
self.day_buy_quantity += quantity;
|
||||||
self.day_buy_value += execution_price * quantity as f64;
|
self.day_buy_value += execution_price * quantity as f64;
|
||||||
|
if previous_quantity > 0
|
||||||
|
&& previous_average_cost.is_finite()
|
||||||
|
&& previous_average_cost > 0.0
|
||||||
|
&& execution_price.is_finite()
|
||||||
|
&& execution_price > 0.0
|
||||||
|
{
|
||||||
|
self.average_cost = (previous_average_cost * previous_quantity as f64
|
||||||
|
+ execution_price * quantity as f64)
|
||||||
|
/ self.quantity as f64;
|
||||||
|
} else {
|
||||||
self.recalculate_average_cost();
|
self.recalculate_average_cost();
|
||||||
|
}
|
||||||
self.refresh_day_pnl();
|
self.refresh_day_pnl();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +130,7 @@ impl Position {
|
|||||||
let mut remaining = quantity;
|
let mut remaining = quantity;
|
||||||
let mut realized = 0.0;
|
let mut realized = 0.0;
|
||||||
let mut realized_entry = 0.0;
|
let mut realized_entry = 0.0;
|
||||||
|
let average_cost_before_sell = self.average_cost;
|
||||||
|
|
||||||
while remaining > 0 {
|
while remaining > 0 {
|
||||||
let Some(first_lot) = self.lots.first_mut() else {
|
let Some(first_lot) = self.lots.first_mut() else {
|
||||||
@@ -141,7 +155,13 @@ impl Position {
|
|||||||
self.day_trade_quantity_delta -= quantity as i32;
|
self.day_trade_quantity_delta -= quantity as i32;
|
||||||
self.day_sell_quantity += quantity;
|
self.day_sell_quantity += quantity;
|
||||||
self.day_sell_value += execution_price * quantity as f64;
|
self.day_sell_value += execution_price * quantity as f64;
|
||||||
|
if self.quantity == 0 {
|
||||||
self.recalculate_average_cost();
|
self.recalculate_average_cost();
|
||||||
|
} else if average_cost_before_sell.is_finite() && average_cost_before_sell > 0.0 {
|
||||||
|
self.average_cost = average_cost_before_sell;
|
||||||
|
} else {
|
||||||
|
self.recalculate_average_cost();
|
||||||
|
}
|
||||||
self.refresh_day_pnl();
|
self.refresh_day_pnl();
|
||||||
Ok(realized)
|
Ok(realized)
|
||||||
}
|
}
|
||||||
@@ -252,8 +272,12 @@ impl Position {
|
|||||||
}
|
}
|
||||||
if let Some(lot) = self.lots.last_mut() {
|
if let Some(lot) = self.lots.last_mut() {
|
||||||
lot.price += cost / quantity as f64;
|
lot.price += cost / quantity as f64;
|
||||||
|
if self.quantity > 0 && self.average_cost.is_finite() && self.average_cost > 0.0 {
|
||||||
|
self.average_cost += cost / self.quantity as f64;
|
||||||
|
} else {
|
||||||
self.recalculate_average_cost();
|
self.recalculate_average_cost();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
self.day_trade_cost += cost;
|
self.day_trade_cost += cost;
|
||||||
self.refresh_day_pnl();
|
self.refresh_day_pnl();
|
||||||
}
|
}
|
||||||
@@ -370,7 +394,11 @@ impl Position {
|
|||||||
self.lots = scaled_lots;
|
self.lots = scaled_lots;
|
||||||
self.quantity = self.lots.iter().map(|lot| lot.quantity).sum();
|
self.quantity = self.lots.iter().map(|lot| lot.quantity).sum();
|
||||||
self.last_price /= ratio;
|
self.last_price /= ratio;
|
||||||
|
if self.average_cost.is_finite() && self.average_cost > 0.0 {
|
||||||
|
self.average_cost /= ratio;
|
||||||
|
} else {
|
||||||
self.recalculate_average_cost();
|
self.recalculate_average_cost();
|
||||||
|
}
|
||||||
self.day_split_ratio *= ratio;
|
self.day_split_ratio *= ratio;
|
||||||
self.refresh_day_pnl();
|
self.refresh_day_pnl();
|
||||||
self.quantity as i32 - old_quantity as i32
|
self.quantity as i32 - old_quantity as i32
|
||||||
@@ -837,6 +865,7 @@ impl PortfolioState {
|
|||||||
|
|
||||||
let old_quantity = old_position.quantity;
|
let old_quantity = old_position.quantity;
|
||||||
let last_price = old_position.last_price;
|
let last_price = old_position.last_price;
|
||||||
|
let old_average_cost = old_position.average_cost;
|
||||||
let realized_pnl = old_position.realized_pnl;
|
let realized_pnl = old_position.realized_pnl;
|
||||||
let realized_entry_pnl = old_position.realized_entry_pnl;
|
let realized_entry_pnl = old_position.realized_entry_pnl;
|
||||||
let mut converted_lots = old_position
|
let mut converted_lots = old_position
|
||||||
@@ -872,6 +901,8 @@ impl PortfolioState {
|
|||||||
.positions
|
.positions
|
||||||
.entry(new_symbol.to_string())
|
.entry(new_symbol.to_string())
|
||||||
.or_insert_with(|| Position::new(new_symbol));
|
.or_insert_with(|| Position::new(new_symbol));
|
||||||
|
let successor_quantity_before = successor.quantity;
|
||||||
|
let successor_average_cost_before = successor.average_cost;
|
||||||
successor.lots.extend(converted_lots);
|
successor.lots.extend(converted_lots);
|
||||||
successor.quantity = successor.lots.iter().map(|lot| lot.quantity).sum();
|
successor.quantity = successor.lots.iter().map(|lot| lot.quantity).sum();
|
||||||
successor.realized_pnl += realized_pnl;
|
successor.realized_pnl += realized_pnl;
|
||||||
@@ -879,7 +910,30 @@ impl PortfolioState {
|
|||||||
if converted_last_price > 0.0 {
|
if converted_last_price > 0.0 {
|
||||||
successor.last_price = converted_last_price;
|
successor.last_price = converted_last_price;
|
||||||
}
|
}
|
||||||
|
let converted_average_cost = if old_average_cost.is_finite()
|
||||||
|
&& old_average_cost > 0.0
|
||||||
|
&& ratio.is_finite()
|
||||||
|
&& ratio > 0.0
|
||||||
|
{
|
||||||
|
Some(old_average_cost / ratio)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(converted_average_cost) = converted_average_cost {
|
||||||
|
if successor_quantity_before > 0
|
||||||
|
&& successor_average_cost_before.is_finite()
|
||||||
|
&& successor_average_cost_before > 0.0
|
||||||
|
{
|
||||||
|
successor.average_cost = (successor_average_cost_before
|
||||||
|
* successor_quantity_before as f64
|
||||||
|
+ converted_average_cost * converted_quantity as f64)
|
||||||
|
/ successor.quantity as f64;
|
||||||
|
} else {
|
||||||
|
successor.average_cost = converted_average_cost;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
successor.recalculate_average_cost();
|
successor.recalculate_average_cost();
|
||||||
|
}
|
||||||
successor.refresh_day_pnl();
|
successor.refresh_day_pnl();
|
||||||
|
|
||||||
Some(SuccessorConversionOutcome {
|
Some(SuccessorConversionOutcome {
|
||||||
@@ -955,6 +1009,45 @@ mod tests {
|
|||||||
assert!((position.holding_return(6.06).unwrap() - (6.06 / 5.66 - 1.0)).abs() < 1e-12);
|
assert!((position.holding_return(6.06).unwrap() - (6.06 / 5.66 - 1.0)).abs() < 1e-12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn partial_sell_preserves_remaining_average_cost() {
|
||||||
|
let date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||||
|
let mut position = Position::new("603958.SH");
|
||||||
|
position.buy(date, 800, 18.0981);
|
||||||
|
position.record_buy_trade_cost(800, 5.0);
|
||||||
|
position.buy(date, 1700, 19.4694);
|
||||||
|
position.record_buy_trade_cost(1700, 8.27451625);
|
||||||
|
position.buy(date, 200, 18.4584);
|
||||||
|
position.record_buy_trade_cost(200, 5.0);
|
||||||
|
position.buy(date, 100, 17.8378);
|
||||||
|
position.record_buy_trade_cost(100, 5.0);
|
||||||
|
let average_cost_before = position.average_cost;
|
||||||
|
|
||||||
|
position.sell(2700, 16.8331).expect("partial sell");
|
||||||
|
|
||||||
|
assert_eq!(position.quantity, 100);
|
||||||
|
assert!((position.average_cost - average_cost_before).abs() < 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn buy_after_partial_sell_continues_moving_average_cost_basis() {
|
||||||
|
let date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||||
|
let mut position = Position::new("300405.SZ");
|
||||||
|
position.buy(date, 100, 10.0);
|
||||||
|
position.buy(date, 100, 5.0);
|
||||||
|
assert!((position.average_cost - 7.5).abs() < 1e-12);
|
||||||
|
|
||||||
|
position.sell(100, 6.0).expect("partial sell");
|
||||||
|
assert_eq!(position.quantity, 100);
|
||||||
|
assert!((position.average_cost - 7.5).abs() < 1e-12);
|
||||||
|
assert!((position.average_entry_price().unwrap() - 5.0).abs() < 1e-12);
|
||||||
|
|
||||||
|
position.buy(date, 100, 5.0);
|
||||||
|
assert_eq!(position.quantity, 200);
|
||||||
|
assert!((position.average_cost - 6.25).abs() < 1e-12);
|
||||||
|
assert!((position.average_entry_price().unwrap() - 5.0).abs() < 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn holdings_summary_reports_entry_price_pnl_excluding_buy_commission() {
|
fn holdings_summary_reports_entry_price_pnl_excluding_buy_commission() {
|
||||||
let date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
let date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||||
@@ -981,7 +1074,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cash_dividend_can_preserve_avg_cost_for_aiquant_compatibility() {
|
fn cash_dividend_can_preserve_avg_cost_for_aiquant_rules() {
|
||||||
let date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
let date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||||
let mut position = Position::new("603102.SH");
|
let mut position = Position::new("603102.SH");
|
||||||
position.buy(date, 1000, 46.45);
|
position.buy(date, 1000, 46.45);
|
||||||
@@ -1033,7 +1126,7 @@ mod tests {
|
|||||||
ask1: 10.01,
|
ask1: 10.01,
|
||||||
prev_close: 9.8,
|
prev_close: 9.8,
|
||||||
volume: 1000,
|
volume: 1000,
|
||||||
tick_volume: 1000,
|
minute_volume: 1000,
|
||||||
bid1_volume: 1000,
|
bid1_volume: 1000,
|
||||||
ask1_volume: 1000,
|
ask1_volume: 1000,
|
||||||
trading_phase: None,
|
trading_phase: None,
|
||||||
@@ -1056,7 +1149,7 @@ mod tests {
|
|||||||
ask1: 10.51,
|
ask1: 10.51,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 1000,
|
volume: 1000,
|
||||||
tick_volume: 1000,
|
minute_volume: 1000,
|
||||||
bid1_volume: 1000,
|
bid1_volume: 1000,
|
||||||
ask1_volume: 1000,
|
ask1_volume: 1000,
|
||||||
trading_phase: None,
|
trading_phase: None,
|
||||||
@@ -1080,6 +1173,7 @@ mod tests {
|
|||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -1144,7 +1238,7 @@ mod tests {
|
|||||||
ask1: 10.51,
|
ask1: 10.51,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 1000,
|
volume: 1000,
|
||||||
tick_volume: 1000,
|
minute_volume: 1000,
|
||||||
bid1_volume: 1000,
|
bid1_volume: 1000,
|
||||||
ask1_volume: 1000,
|
ask1_volume: 1000,
|
||||||
trading_phase: None,
|
trading_phase: None,
|
||||||
@@ -1167,6 +1261,7 @@ mod tests {
|
|||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -1228,7 +1323,7 @@ mod tests {
|
|||||||
ask1: 10.31,
|
ask1: 10.31,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 1000,
|
volume: 1000,
|
||||||
tick_volume: 1000,
|
minute_volume: 1000,
|
||||||
bid1_volume: 1000,
|
bid1_volume: 1000,
|
||||||
ask1_volume: 1000,
|
ask1_volume: 1000,
|
||||||
trading_phase: None,
|
trading_phase: None,
|
||||||
@@ -1297,7 +1392,7 @@ mod tests {
|
|||||||
ask1: 3.02,
|
ask1: 3.02,
|
||||||
prev_close: 2.98,
|
prev_close: 2.98,
|
||||||
volume: 152_975,
|
volume: 152_975,
|
||||||
tick_volume: 152_975,
|
minute_volume: 152_975,
|
||||||
bid1_volume: 338,
|
bid1_volume: 338,
|
||||||
ask1_volume: 2476,
|
ask1_volume: 2476,
|
||||||
trading_phase: None,
|
trading_phase: None,
|
||||||
@@ -1320,7 +1415,7 @@ mod tests {
|
|||||||
ask1: 3.07,
|
ask1: 3.07,
|
||||||
prev_close: 3.06,
|
prev_close: 3.06,
|
||||||
volume: 160_000,
|
volume: 160_000,
|
||||||
tick_volume: 160_000,
|
minute_volume: 160_000,
|
||||||
bid1_volume: 1000,
|
bid1_volume: 1000,
|
||||||
ask1_volume: 1000,
|
ask1_volume: 1000,
|
||||||
trading_phase: None,
|
trading_phase: None,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,10 @@ impl RuleCheck {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait EquityRuleHooks {
|
pub trait EquityRuleHooks {
|
||||||
|
fn duplicates_standard_china_risk(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
fn can_buy(
|
fn can_buy(
|
||||||
&self,
|
&self,
|
||||||
execution_date: NaiveDate,
|
execution_date: NaiveDate,
|
||||||
@@ -49,6 +53,10 @@ pub trait EquityRuleHooks {
|
|||||||
pub struct ChinaEquityRuleHooks;
|
pub struct ChinaEquityRuleHooks;
|
||||||
|
|
||||||
impl EquityRuleHooks for ChinaEquityRuleHooks {
|
impl EquityRuleHooks for ChinaEquityRuleHooks {
|
||||||
|
fn duplicates_standard_china_risk(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn can_buy(
|
fn can_buy(
|
||||||
&self,
|
&self,
|
||||||
_execution_date: NaiveDate,
|
_execution_date: NaiveDate,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ pub enum ScheduleStage {
|
|||||||
BeforeTrading,
|
BeforeTrading,
|
||||||
OpenAuction,
|
OpenAuction,
|
||||||
Bar,
|
Bar,
|
||||||
Tick,
|
Minute,
|
||||||
OnDay,
|
OnDay,
|
||||||
AfterTrading,
|
AfterTrading,
|
||||||
Settlement,
|
Settlement,
|
||||||
@@ -225,7 +225,7 @@ pub fn default_stage_time(stage: ScheduleStage) -> Option<NaiveTime> {
|
|||||||
ScheduleStage::BeforeTrading => Some(NaiveTime::from_hms_opt(9, 0, 0).expect("valid time")),
|
ScheduleStage::BeforeTrading => Some(NaiveTime::from_hms_opt(9, 0, 0).expect("valid time")),
|
||||||
ScheduleStage::OpenAuction => Some(NaiveTime::from_hms_opt(9, 31, 0).expect("valid time")),
|
ScheduleStage::OpenAuction => Some(NaiveTime::from_hms_opt(9, 31, 0).expect("valid time")),
|
||||||
ScheduleStage::Bar => Some(NaiveTime::from_hms_opt(10, 18, 0).expect("valid time")),
|
ScheduleStage::Bar => Some(NaiveTime::from_hms_opt(10, 18, 0).expect("valid time")),
|
||||||
ScheduleStage::Tick => None,
|
ScheduleStage::Minute => None,
|
||||||
ScheduleStage::OnDay => Some(NaiveTime::from_hms_opt(10, 18, 0).expect("valid time")),
|
ScheduleStage::OnDay => Some(NaiveTime::from_hms_opt(10, 18, 0).expect("valid time")),
|
||||||
ScheduleStage::AfterTrading => Some(NaiveTime::from_hms_opt(15, 0, 0).expect("valid time")),
|
ScheduleStage::AfterTrading => Some(NaiveTime::from_hms_opt(15, 0, 0).expect("valid time")),
|
||||||
ScheduleStage::Settlement => Some(NaiveTime::from_hms_opt(15, 1, 0).expect("valid time")),
|
ScheduleStage::Settlement => Some(NaiveTime::from_hms_opt(15, 1, 0).expect("valid time")),
|
||||||
|
|||||||
+459
-343
File diff suppressed because it is too large
Load Diff
@@ -69,7 +69,30 @@ pub struct StrategyAiCatalog {
|
|||||||
pub indicator_factors: Vec<String>,
|
pub indicator_factors: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
pub clickhouse_table_fields: Vec<ManualFactorSource>,
|
pub data_lake_fields: Vec<ManualFactorSource>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct StrategyAiHoldingCountContract {
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
alias = "holdingCount",
|
||||||
|
alias = "holding_count",
|
||||||
|
alias = "targetHoldingCount",
|
||||||
|
alias = "target_holding_count"
|
||||||
|
)]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub count: Option<i64>,
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
alias = "kind",
|
||||||
|
alias = "holdingCountMode",
|
||||||
|
alias = "holding_count_mode",
|
||||||
|
alias = "targetHoldingCountMode",
|
||||||
|
alias = "target_holding_count_mode"
|
||||||
|
)]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub mode: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -79,6 +102,9 @@ pub struct StrategyAiGenerateRequest {
|
|||||||
pub market: String,
|
pub market: String,
|
||||||
pub benchmark_symbol: String,
|
pub benchmark_symbol: String,
|
||||||
pub signal_symbol: String,
|
pub signal_symbol: String,
|
||||||
|
#[serde(default, alias = "holdingCountContract")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub holding_count_contract: Option<StrategyAiHoldingCountContract>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -87,8 +113,15 @@ pub struct StrategyAiOptimizeRequest {
|
|||||||
pub objective: String,
|
pub objective: String,
|
||||||
pub result_summary: serde_json::Value,
|
pub result_summary: serde_json::Value,
|
||||||
pub diagnostics: Vec<String>,
|
pub diagnostics: Vec<String>,
|
||||||
|
#[serde(default, alias = "holdingCountContract")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub holding_count_contract: Option<StrategyAiHoldingCountContract>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_THREE_YEAR_RETURN_TARGET_PROMPT: &str = "默认收益目标:用户没有明确指定更高收益阈值时,三年回测区间策略总收益严格 > 120% 才视为满足收益目标,120.00% 本身不算达标;达到该阈值后可以继续优化夏普、回撤、换手和稳定性,但不得把已达标策略判为失败或为了追更高收益破坏无未来数据、持仓数量和同条件对账合同。";
|
||||||
|
const DEFAULT_RISK_POLICY_DSL_PROMPT: &str = "reject_st_selection=false、reject_st_buy=true、reject_star_st_selection=false、reject_star_st_buy=true、reject_paused_selection=false、reject_paused_buy=true、reject_paused_sell=true、reject_inactive_selection=false、reject_inactive_buy=true、reject_inactive_sell=true、reject_new_listing_selection=false、reject_new_listing_buy=true、reject_kcb_selection=false、reject_kcb_buy=true、reject_bjse_selection=false、reject_bjse_buy=true、reject_one_yuan_selection=false、reject_one_yuan_buy=true、respect_allow_buy_sell=true、reject_upper_limit_selection=false、reject_lower_limit_selection=false、reject_upper_limit_buy=true、reject_lower_limit_sell=true、forbid_same_day_rebuy_after_sell=true、blacklist_enabled=true、allow_market_orders=true、live_trading_enabled=false、volume_limit_enabled=true、liquidity_limit_enabled=true、volume_percent=0.25、commission_rate=0.0003、minimum_commission=5、stamp_tax_rate_before_change=0.001、stamp_tax_rate_after_change=0.0005、stamp_tax_change_date=\"2023-08-28\"";
|
||||||
|
const DEFAULT_RISK_POLICY_DSL_CODE: &str = "reject_st_selection=false, reject_st_buy=true, reject_star_st_selection=false, reject_star_st_buy=true, reject_paused_selection=false, reject_paused_buy=true, reject_paused_sell=true, reject_inactive_selection=false, reject_inactive_buy=true, reject_inactive_sell=true, reject_new_listing_selection=false, reject_new_listing_buy=true, reject_kcb_selection=false, reject_kcb_buy=true, reject_bjse_selection=false, reject_bjse_buy=true, reject_one_yuan_selection=false, reject_one_yuan_buy=true, respect_allow_buy_sell=true, reject_upper_limit_selection=false, reject_lower_limit_selection=false, reject_upper_limit_buy=true, reject_lower_limit_sell=true, forbid_same_day_rebuy_after_sell=true, blacklist_enabled=true, allow_market_orders=true, live_trading_enabled=false, volume_limit_enabled=true, liquidity_limit_enabled=true, volume_percent=0.25, commission_rate=0.0003, minimum_commission=5, stamp_tax_rate_before_change=0.001, stamp_tax_rate_after_change=0.0005, stamp_tax_change_date=\"2023-08-28\"";
|
||||||
|
|
||||||
pub fn built_in_strategy_manual() -> StrategyAiManual {
|
pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||||
StrategyAiManual {
|
StrategyAiManual {
|
||||||
title: "OmniQuant 平台策略脚本手册".to_string(),
|
title: "OmniQuant 平台策略脚本手册".to_string(),
|
||||||
@@ -97,11 +130,12 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
|||||||
"平台策略脚本采用声明式 DSL + 表达式执行模型。".to_string(),
|
"平台策略脚本采用声明式 DSL + 表达式执行模型。".to_string(),
|
||||||
"支持 let 变量、fn 自定义函数、when/unless/else 条件块、可用指标/因子字段映射。".to_string(),
|
"支持 let 变量、fn 自定义函数、when/unless/else 条件块、可用指标/因子字段映射。".to_string(),
|
||||||
"支持数值型和字符串型因子,字符串字段可用于行业、概念、标签、板块等分类过滤。".to_string(),
|
"支持数值型和字符串型因子,字符串字段可用于行业、概念、标签、板块等分类过滤。".to_string(),
|
||||||
"当前默认回测数据已支持 OHLCV、市值、流通市值、换手率、有效换手率、上市天数、停牌/ST/板块、涨跌停价格、tick 触达涨跌停、常用价格/成交量均线,以及 stock_indicator_factors_v1 中已入库的通用指标因子。".to_string(),
|
"用户明确指定目标持仓数量或最低持仓数量时,selection.limit 必须严格表达该数量;不要因为优化收益、减少交易或转换框架而擅自改小持仓数。".to_string(),
|
||||||
|
"当前默认回测数据已支持 OHLCV、市值、流通市值、换手率、有效换手率、上市天数、停牌/ST/板块、涨跌停价格、分钟线触达涨跌停、常用价格/成交量均线,以及 stock_indicator_factors_v1 中已入库的通用指标因子。".to_string(),
|
||||||
"AI 生成策略时只能输出完整 engine-script 代码,不输出 Markdown、解释、推理过程、JSON 包装或手册复述。".to_string(),
|
"AI 生成策略时只能输出完整 engine-script 代码,不输出 Markdown、解释、推理过程、JSON 包装或手册复述。".to_string(),
|
||||||
"表达式字段以运行时字段为准:市值使用 market_cap,流通市值使用 free_float_cap;不要在策略表达式中使用数据库原始字段 float_market_cap。".to_string(),
|
"表达式字段以运行时字段为准:市值使用 market_cap,流通市值使用 free_float_cap;不要在策略表达式中使用数据库原始字段 float_market_cap。".to_string(),
|
||||||
"任意窗口价格均线使用 rolling_mean(\"close\", n) 或 ma(\"close\", n),任意窗口均量使用 rolling_mean(\"volume\", n) 或 vma(n);不要使用未列出的 ma60、stock_ma60、signal_ma60 或 benchmark_ma60 变量。".to_string(),
|
"任意窗口价格均线使用 rolling_mean(\"close\", n) 或 ma(\"close\", n),任意窗口均量使用 rolling_mean(\"volume\", n) 或 vma(n);不要使用未列出的 ma60、stock_ma60、signal_ma60 或 benchmark_ma60 变量。".to_string(),
|
||||||
"next_bar_open 会用决策日信号生成订单,并在下一可交易开盘撮合;不得把执行日 open/high/low/close 当成下单前已知信息。".to_string(),
|
"next_bar_open 会用决策日信号生成订单,并在下一可交易开盘撮合;不得把执行日 open/high/low/close 当成下单前已知信息;涨停买入和跌停卖出风控必须用实际 next-open 成交价比较,不能用执行日 close/last 或 next-close。".to_string(),
|
||||||
"自定义 fn 必须通过参数传入运行时字段;不要用 fn score() 这类零参数函数直接引用 market_cap、close、ma5 等股票字段。".to_string(),
|
"自定义 fn 必须通过参数传入运行时字段;不要用 fn score() 这类零参数函数直接引用 market_cap、close、ma5 等股票字段。".to_string(),
|
||||||
"禁止自由 Python/JavaScript 命令式语句,最终必须输出平台 DSL。".to_string(),
|
"禁止自由 Python/JavaScript 命令式语句,最终必须输出平台 DSL。".to_string(),
|
||||||
],
|
],
|
||||||
@@ -203,8 +237,8 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
|||||||
detail: "支持按交易周或交易月调仓,例如 rebalance.weekly(weekday=5).at([\"10:18\"])、rebalance.weekly(tradingday=-1).at([\"10:18\"])、rebalance.monthly(tradingday=1).at([\"10:18\"])。`.at([...])` 的最后一个时刻会编进分钟级 schedule/time_rule;当前平台把 on_day 近似到 10:18,把 open_auction 近似到 09:31。".to_string(),
|
detail: "支持按交易周或交易月调仓,例如 rebalance.weekly(weekday=5).at([\"10:18\"])、rebalance.weekly(tradingday=-1).at([\"10:18\"])、rebalance.monthly(tradingday=1).at([\"10:18\"])。`.at([...])` 的最后一个时刻会编进分钟级 schedule/time_rule;当前平台把 on_day 近似到 10:18,把 open_auction 近似到 09:31。".to_string(),
|
||||||
},
|
},
|
||||||
ManualSection {
|
ManualSection {
|
||||||
title: "bar / tick 生命周期".to_string(),
|
title: "bar / minute execution 生命周期".to_string(),
|
||||||
detail: "回测内核支持 平台内核 风格的 bar/tick 生命周期:日内会发布 pre_bar/bar/post_bar 过程事件;存在 tick 订阅或 tick 调度规则时,会按 execution_quotes 的时间顺序发布 pre_tick/tick/post_tick,并把 tick 阶段下单限制在当前 tick 时间窗内撮合。平台 DSL 中可通过 subscribe([...])、trading.subscription_guard(true) 和 process_event 字段配合显式订单模拟 tick 订阅策略。".to_string(),
|
detail: "回测内核支持 平台内核 风格的 bar/分钟执行价生命周期:日内会发布 pre_bar/bar/post_bar 过程事件;存在分钟执行价订阅或分钟调度规则时,会按 execution_quotes 的时间顺序发布 pre_minute/minute/post_minute 过程事件,并把日内阶段下单限制在当前分钟执行价时间窗内撮合。平台 DSL 中可通过 subscribe([...])、trading.subscription_guard(true) 和 process_event 字段配合显式订单模拟日内订阅策略。".to_string(),
|
||||||
},
|
},
|
||||||
ManualSection {
|
ManualSection {
|
||||||
title: "selection.market_cap_band / selection.limit / ordering.rank_by / ordering.rank_expr".to_string(),
|
title: "selection.market_cap_band / selection.limit / ordering.rank_by / ordering.rank_expr".to_string(),
|
||||||
@@ -214,21 +248,25 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
|||||||
title: "filter.stock_expr / risk.stop_loss / risk.take_profit / allocation.buy_scale".to_string(),
|
title: "filter.stock_expr / risk.stop_loss / risk.take_profit / allocation.buy_scale".to_string(),
|
||||||
detail: "表达式型规则,支持多条组合。stop_loss/take_profit 多条按 OR 组合,filter.stock_expr 多条按 AND 组合。".to_string(),
|
detail: "表达式型规则,支持多条组合。stop_loss/take_profit 多条按 OR 组合,filter.stock_expr 多条按 AND 组合。".to_string(),
|
||||||
},
|
},
|
||||||
|
ManualSection {
|
||||||
|
title: "risk.policy / risk.blacklist".to_string(),
|
||||||
|
detail: "统一配置 FIDC 基础风控。risk.policy(...) 支持 reject_st_selection、reject_st_buy、reject_star_st_selection、reject_star_st_buy、reject_paused_selection、reject_paused_buy、reject_paused_sell、reject_inactive_selection、reject_inactive_buy、reject_inactive_sell、reject_new_listing_selection、reject_new_listing_buy、reject_kcb_selection、reject_kcb_buy、reject_bjse_selection、reject_bjse_buy、reject_one_yuan_selection、reject_one_yuan_buy、respect_allow_buy_sell、reject_upper_limit_selection、reject_lower_limit_selection、reject_upper_limit_buy、reject_lower_limit_sell、forbid_same_day_rebuy_after_sell、blacklist_enabled、allow_market_orders、live_trading_enabled、blacklisted_symbols、volume_limit_enabled、liquidity_limit_enabled、volume_percent、commission_rate、minimum_commission、stamp_tax_rate_before_change、stamp_tax_rate_after_change、stamp_tax_change_date 等命名参数;risk.blacklist([\"600000.SH\"]) 写策略级黑名单。ST、*ST、停牌、退市、科创、北交所、一元、涨跌停、同日卖出禁买、黑名单、成交量和费用等基础风控必须走 risk.policy 或运行态 RiskLimits,不要写进 universe.exclude 或 filter.stock_expr。PG/Source Lake 是真相源,Redis 只可做当日锁、热配置缓存和配置变更通知。".to_string(),
|
||||||
|
},
|
||||||
ManualSection {
|
ManualSection {
|
||||||
title: "corporate_actions.dividend_reinvestment".to_string(),
|
title: "corporate_actions.dividend_reinvestment".to_string(),
|
||||||
detail: "支持 corporate_actions.dividend_reinvestment(true)。开启后,现金分红到账会优先按 round lot 回补成同一只股票,零头保留为现金。".to_string(),
|
detail: "支持 corporate_actions.dividend_reinvestment(true)。开启后,现金分红到账会优先按 round lot 回补成同一只股票,零头保留为现金。".to_string(),
|
||||||
},
|
},
|
||||||
ManualSection {
|
ManualSection {
|
||||||
title: "execution.matching_type / execution.slippage".to_string(),
|
title: "execution.matching_type / execution.slippage".to_string(),
|
||||||
detail: "设置撮合模式和滑点。支持 execution.matching_type(\"next_tick_last\" | \"next_tick_best_own\" | \"next_tick_best_counterparty\" | \"counterparty_offer\" | \"vwap\" | \"current_bar_close\" | \"next_bar_open\" | \"open_auction\")。其中 next_tick_last 使用 tick 的 last_price;next_tick_best_own / next_tick_best_counterparty 会按 L1 买一卖一近似 平台内核 的 tick 最优价语义;counterparty_offer 在存在 order_book_depth 多档盘口数据时会按真实档位逐档扫单并计算加权成交价,不存在 depth 时回退 L1 对手方报价;vwap 会在盘中执行价链路上聚合多笔成交为单条 VWAP 成交;next_bar_open 使用决策日信号并在下一可交易日开盘撮合,禁止把执行日 open/high/low/close 解释为下单前已知数据;open_auction 使用当日集合竞价开盘价 day_open 进行撮合,且不额外施加滑点,并按竞价成交量而不是盘口一档流动性限制成交;滑点支持 execution.slippage(\"none\") / execution.slippage(\"price_ratio\", 0.001) / execution.slippage(\"tick_size\", 1) / execution.slippage(\"limit_price\"),其中 limit_price 会在限价单成交时按挂单价模拟 平台内核 的最坏成交价。".to_string(),
|
detail: "设置回测全局撮合模式和滑点。日线回测只允许 execution.matching_type(\"current_bar_close\") 或 execution.matching_type(\"next_bar_open\");current_bar_close 使用决策日当日 close,next_bar_open 使用决策日信号并在下一可交易日 open 撮合,禁止把执行日 open/high/low/close 解释为下单前已知数据;next_bar_open 的涨停买入和跌停卖出判断必须比较实际 open 成交价与涨跌停价,不能用执行日 close/last 或 next-close。分钟线回测使用当前分钟价格成交,只能写 execution.matching_type(\"minute_last\");不要把 vwap、twap、open_auction、minute_best_own、minute_best_counterparty 写成全局 matching_type,这些只属于显式订单或内部撮合能力。日线调仓现金口径由 execution.rebalance_cash_mode(\"sell_then_buy\" | \"same_point_net\" | \"pre_open_cash\") 或页面/API 参数控制,默认 sell_then_buy;sell_then_buy_delay_slippage_rate 只来自页面/API 执行参数,默认 0,不要写进策略表达式。滑点支持 execution.slippage(\"none\") / execution.slippage(\"price_ratio\", 0.001) / execution.slippage(\"tick_size\", 1) / execution.slippage(\"limit_price\"),其中 limit_price 会在限价单成交时按挂单价模拟 平台内核 的最坏成交价。".to_string(),
|
||||||
},
|
},
|
||||||
ManualSection {
|
ManualSection {
|
||||||
title: "期货提交校验".to_string(),
|
title: "期货提交校验".to_string(),
|
||||||
detail: "期货订单进入撮合前会先执行账户与交易规则校验:合约必须在上市/退市日期范围内,日行情不能停牌,trading_phase 需处于 continuous/trading/open_auction/auction/call_auction/opening_auction 等可交易阶段,限价必须为正且按 futures_trading_parameters.price_tick 或日行情 price_tick 对齐,并且不能越过 upper_limit/lower_limit;随后继续检查反向挂单自成交风险、保证金和可平数量。服务层可通过 FuturesValidationConfig 分别关闭 active instrument、trading phase、limit price tick、price limit 校验,用于兼容特殊数据,但默认全部开启。".to_string(),
|
detail: "期货订单进入撮合前会先执行账户与交易规则校验:合约必须在上市/退市日期范围内,日行情不能停牌,trading_phase 需处于 continuous/trading/open_auction/auction/call_auction/opening_auction 等可交易阶段,限价必须为正且按 futures_trading_parameters.price_tick 或日行情 price_tick 对齐,并且不能越过 upper_limit/lower_limit;随后继续检查反向挂单自成交风险、保证金和可平数量。服务层可通过 FuturesValidationConfig 分别关闭 active instrument、trading phase、限价最小价位、price limit 校验,但默认全部开启。".to_string(),
|
||||||
},
|
},
|
||||||
ManualSection {
|
ManualSection {
|
||||||
title: "trading.rotation / order.* / cancel.* / update_universe / subscribe".to_string(),
|
title: "trading.rotation / order.* / cancel.* / update_universe / subscribe".to_string(),
|
||||||
detail: "支持显式下单、撤单、AlgoOrder、动态 universe 和账户资金动作。可以用 trading.rotation(false) 关闭默认轮动链路,再用 trading.stage(\"open_auction\" | \"on_day\") 指定执行阶段;需要模拟 平台内核 的 tick 订阅保护时,可写 trading.subscription_guard(true),未订阅 symbol 的显式订单会被拦截,TargetPortfolioSmart + AlgoOrder 会过滤未订阅标的。用 trading.schedule.daily().at([\"10:18\"]) / trading.schedule.weekly(weekday=5).at([\"10:18\"]) / trading.schedule.weekly(tradingday=-1).at([\"10:18\"]) / trading.schedule.monthly(tradingday=1).at([\"10:18\"]) 指定触发频率和分钟级 time_rule,然后写 order.shares(\"600000.SH\", 1000)、order.target_shares(\"600000.SH\", 2000)、order.value(\"600000.SH\", cash * 0.25)、order.target_percent(\"600000.SH\", 0.05)、order.limit_value(\"600000.SH\", cash * 0.25, open * 0.99)、order.vwap_value(\"600000.SH\", cash * 0.25, \"09:31\", \"09:40\")、order.twap_percent(\"600000.SH\", 0.05, \"10:00\", \"10:30\")、order.target_portfolio_smart(weights={\"600000.SH\": 0.3, \"000001.SZ\": 0.2}, order_prices=VWAPOrder(930, 940), valuation_prices={\"600000.SH\": prev_close})、order.target_portfolio_smart(weights={\"600000.SH\": 0.3, \"000001.SZ\": 0.2}, order_prices={\"600000.SH\": open * 0.99}, valuation_prices={\"600000.SH\": prev_close})、cancel.order(12345)、cancel.symbol(\"600000.SH\")、cancel.all()、update_universe([\"600000.SH\", \"000001.SZ\"])、subscribe([\"000001.SZ\"])、unsubscribe([\"000001.SZ\"])、account.deposit_withdraw(100000, receiving_days=0)、account.finance_repay(50000)、account.set_management_fee_rate(0.001)。其中 order.target_shares(...) 对应 平台内核 的 order_to,order.target_portfolio_smart(...) 对应 平台内核 的 order_target_portfolio_smart 批量目标权重语义;account.deposit_withdraw(...) 和 account.finance_repay(...) 对应 平台内核 账户出入金与融资/还款语义;order_prices 既可以是逐标的限价映射,也可以是 VWAPOrder/TWAPOrder 这类全局 AlgoOrder;order.vwap_* / order.twap_* 对应 平台内核 的 AlgoOrder 时间窗订单风格,而 update_universe/subscribe/unsubscribe 对应 平台内核 的动态 universe 与订阅接口。symbol 使用标准证券代码;数量、金额、仓位、时间窗、限价、order_id 和 symbol 列表都支持表达式;这些语句也支持放进 when/unless 条件块。".to_string(),
|
detail: "支持显式下单、撤单、AlgoOrder、动态 universe 和账户资金动作。可以用 trading.rotation(false) 关闭默认轮动链路,再用 trading.stage(\"open_auction\" | \"on_day\") 指定执行阶段;需要模拟 平台内核 的日内订阅保护时,可写 trading.subscription_guard(true),未订阅 symbol 的显式订单会被拦截,TargetPortfolioSmart + AlgoOrder 会过滤未订阅标的。用 trading.schedule.daily().at([\"10:18\"]) / trading.schedule.weekly(weekday=5).at([\"10:18\"]) / trading.schedule.weekly(tradingday=-1).at([\"10:18\"]) / trading.schedule.monthly(tradingday=1).at([\"10:18\"]) 指定触发频率和分钟级 time_rule,然后写 order.shares(\"600000.SH\", 1000)、order.target_shares(\"600000.SH\", 2000)、order.value(\"600000.SH\", cash * 0.25)、order.target_percent(\"600000.SH\", 0.05)、order.limit_value(\"600000.SH\", cash * 0.25, open * 0.99)、order.vwap_value(\"600000.SH\", cash * 0.25, \"09:31\", \"09:40\")、order.twap_percent(\"600000.SH\", 0.05, \"10:00\", \"10:30\")、order.target_portfolio_smart(weights={\"600000.SH\": 0.3, \"000001.SZ\": 0.2}, order_prices=VWAPOrder(930, 940), valuation_prices={\"600000.SH\": prev_close})、order.target_portfolio_smart(weights={\"600000.SH\": 0.3, \"000001.SZ\": 0.2}, order_prices={\"600000.SH\": open * 0.99}, valuation_prices={\"600000.SH\": prev_close})、cancel.order(12345)、cancel.symbol(\"600000.SH\")、cancel.all()、update_universe([\"600000.SH\", \"000001.SZ\"])、subscribe([\"000001.SZ\"])、unsubscribe([\"000001.SZ\"])、account.deposit_withdraw(100000, receiving_days=0)、account.finance_repay(50000)、account.set_management_fee_rate(0.001)。其中 order.target_shares(...) 对应 平台内核 的 order_to,order.target_portfolio_smart(...) 对应 平台内核 的 order_target_portfolio_smart 批量目标权重语义;account.deposit_withdraw(...) 和 account.finance_repay(...) 对应 平台内核 账户出入金与融资/还款语义;order_prices 既可以是逐标的限价映射,也可以是 VWAPOrder/TWAPOrder 这类全局 AlgoOrder;order.vwap_* / order.twap_* 对应 平台内核 的 AlgoOrder 时间窗订单风格,而 update_universe/subscribe/unsubscribe 对应 平台内核 的动态 universe 与订阅接口。symbol 使用标准证券代码;数量、金额、仓位、时间窗、限价、order_id 和 symbol 列表都支持表达式;这些语句也支持放进 when/unless 条件块。".to_string(),
|
||||||
},
|
},
|
||||||
ManualSection {
|
ManualSection {
|
||||||
title: "when / unless / else".to_string(),
|
title: "when / unless / else".to_string(),
|
||||||
@@ -267,12 +305,12 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
|||||||
fields: vec![
|
fields: vec![
|
||||||
ManualField { name: "symbol".to_string(), field_type: "string".to_string(), detail: "证券代码。".to_string() },
|
ManualField { name: "symbol".to_string(), field_type: "string".to_string(), detail: "证券代码。".to_string() },
|
||||||
ManualField { name: "market_cap/free_float_cap".to_string(), field_type: "float".to_string(), detail: "总市值、流通市值。".to_string() },
|
ManualField { name: "market_cap/free_float_cap".to_string(), field_type: "float".to_string(), detail: "总市值、流通市值。".to_string() },
|
||||||
ManualField { name: "turnover/turnover_ratio/effective_turnover_ratio".to_string(), field_type: "float".to_string(), detail: "换手率、换手率标准字段、有效换手率;turnover 是 turnover_ratio 的兼容别名。".to_string() },
|
ManualField { name: "turnover_ratio/effective_turnover_ratio".to_string(), field_type: "float".to_string(), detail: "换手率标准字段和有效换手率。".to_string() },
|
||||||
ManualField { name: "open/high/low/close/last/last_price/prev_close/amount".to_string(), field_type: "float".to_string(), detail: "开盘、最高、最低、收盘、盘中价、昨收和成交额。".to_string() },
|
ManualField { name: "open/high/low/close/last/last_price/prev_close/amount".to_string(), field_type: "float".to_string(), detail: "开盘、最高、最低、收盘、盘中价、昨收和成交额。".to_string() },
|
||||||
ManualField { name: "upper_limit/lower_limit/price_tick/round_lot/minimum_order_quantity/order_step_size".to_string(), field_type: "float/int".to_string(), detail: "涨跌停、最小价位、整手、最小下单量和数量步长。KSH/BJSE 等板块可与 round_lot 不同。".to_string() },
|
ManualField { name: "upper_limit/lower_limit/price_tick/round_lot/minimum_order_quantity/order_step_size".to_string(), field_type: "float/int".to_string(), detail: "涨跌停、最小价位、整手、最小下单量和数量步长。KSH/BJSE 等板块可与 round_lot 不同。".to_string() },
|
||||||
ManualField { name: "paused/is_st/is_kcb/is_one_yuan/is_new_listing".to_string(), field_type: "bool".to_string(), detail: "可交易性与板块标志。".to_string() },
|
ManualField { name: "paused/is_st/is_star_st/is_kcb/is_one_yuan/is_new_listing".to_string(), field_type: "bool".to_string(), detail: "可交易性与板块标志,ST 与 *ST 是独立字段。".to_string() },
|
||||||
ManualField { name: "allow_buy/allow_sell/at_upper_limit/at_lower_limit".to_string(), field_type: "bool".to_string(), detail: "盘中买卖与涨跌停状态。".to_string() },
|
ManualField { name: "allow_buy/allow_sell/at_upper_limit/at_lower_limit".to_string(), field_type: "bool".to_string(), detail: "盘中买卖与涨跌停状态。".to_string() },
|
||||||
ManualField { name: "touched_upper_limit/touched_lower_limit/hit_upper_limit/hit_lower_limit".to_string(), field_type: "bool".to_string(), detail: "当日 tick 曾经触达涨跌停。".to_string() },
|
ManualField { name: "touched_upper_limit/touched_lower_limit/hit_upper_limit/hit_lower_limit".to_string(), field_type: "bool".to_string(), detail: "当日分钟执行价曾经触达涨跌停。".to_string() },
|
||||||
ManualField { name: "symbol_open_order_count/symbol_open_buy_qty/symbol_open_sell_qty/latest_symbol_open_order_id".to_string(), field_type: "int".to_string(), detail: "当前证券在挂单簿中的未成交挂单摘要和最近挂单 id。".to_string() },
|
ManualField { name: "symbol_open_order_count/symbol_open_buy_qty/symbol_open_sell_qty/latest_symbol_open_order_id".to_string(), field_type: "int".to_string(), detail: "当前证券在挂单簿中的未成交挂单摘要和最近挂单 id。".to_string() },
|
||||||
ManualField { name: "latest_symbol_open_order_status/latest_symbol_open_order_unfilled_qty".to_string(), field_type: "string/int".to_string(), detail: "当前证券最近一笔挂单的状态和未成交数量。".to_string() },
|
ManualField { name: "latest_symbol_open_order_status/latest_symbol_open_order_unfilled_qty".to_string(), field_type: "string/int".to_string(), detail: "当前证券最近一笔挂单的状态和未成交数量。".to_string() },
|
||||||
ManualField { name: "in_dynamic_universe/is_subscribed".to_string(), field_type: "bool".to_string(), detail: "当前证券是否在动态 universe 内,以及是否仍在订阅集合中。".to_string() },
|
ManualField { name: "in_dynamic_universe/is_subscribed".to_string(), field_type: "bool".to_string(), detail: "当前证券是否在动态 universe 内,以及是否仍在订阅集合中。".to_string() },
|
||||||
@@ -304,13 +342,13 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
|||||||
functions: vec![
|
functions: vec![
|
||||||
ManualFunction { name: "factor".to_string(), signature: "factor(\"column_name\")".to_string(), detail: "读取当前股票当日可用因子列。数值因子返回 float,字符串因子返回 string;缺失字段默认返回 0 或空字符串,建议重要条件配合 diagnostics 查看候选过滤数量。".to_string() },
|
ManualFunction { name: "factor".to_string(), signature: "factor(\"column_name\")".to_string(), detail: "读取当前股票当日可用因子列。数值因子返回 float,字符串因子返回 string;缺失字段默认返回 0 或空字符串,建议重要条件配合 diagnostics 查看候选过滤数量。".to_string() },
|
||||||
ManualFunction { name: "day_factor".to_string(), signature: "day_factor(\"field_name\")".to_string(), detail: "读取日级/指数级字段映射。".to_string() },
|
ManualFunction { name: "day_factor".to_string(), signature: "day_factor(\"field_name\")".to_string(), detail: "读取日级/指数级字段映射。".to_string() },
|
||||||
ManualFunction { name: "history_bars".to_string(), signature: "ctx.history_bars(symbol, count, \"1d\" | \"1m\" | \"tick\", \"close\", include_now)".to_string(), detail: "回测内核策略上下文数据 API,返回指定证券最近 N 条数值序列。日线字段支持 open/high/low/close/last/prev_close/volume/upper_limit/lower_limit;分钟或 tick 字段支持 last/bid1/ask1/volume_delta/amount_delta。日线 include_now=false 排除当前交易日;分钟/tick 会按当前 on_bar、on_tick 或调度时刻截断,include_now=false 排除当前 bar/tick,避免未来函数。".to_string() },
|
ManualFunction { name: "history_bars".to_string(), signature: "ctx.history_bars(symbol, count, \"1d\" | \"1m\", \"close\", include_now)".to_string(), detail: "回测内核策略上下文数据 API,返回指定证券最近 N 条数值序列。日线字段支持 open/high/low/close/last/prev_close/volume/upper_limit/lower_limit;分钟字段支持 last/bid1/ask1/volume_delta/amount_delta。日线 include_now=false 排除当前信号日;分钟线会按当前 on_bar、日内事件或调度时刻截断,include_now=false 排除当前分钟执行价。next_bar_open 下该 API 只能看到信号日数据,不能读取实际成交日数据。".to_string() },
|
||||||
ManualFunction { name: "current_snapshot".to_string(), signature: "ctx.current_snapshot(symbol)".to_string(), detail: "读取当前交易日指定证券的日级快照,可用于获得当日 open/close/last/upper_limit/lower_limit 等字段。".to_string() },
|
ManualFunction { name: "current_snapshot".to_string(), signature: "ctx.current_snapshot(symbol)".to_string(), detail: "读取当前信号日指定证券的日级快照,可用于获得信号日 open/close/last/upper_limit/lower_limit 等字段;next_bar_open 的实际成交日涨跌停、停牌、ST、退市、一元、黑名单、成交量和盘口流动性由撮合层按执行日判断。".to_string() },
|
||||||
ManualFunction { name: "instrument/instruments/all_instruments".to_string(), signature: "ctx.instrument(symbol)".to_string(), detail: "读取证券元数据,包括名称、板块、上市日期、退市日期、最小下单量、整手、最小价位等;all_instruments 按证券代码稳定排序返回全量证券。".to_string() },
|
ManualFunction { name: "instrument/instruments/all_instruments".to_string(), signature: "ctx.instrument(symbol)".to_string(), detail: "读取证券元数据,包括名称、板块、上市日期、退市日期、最小下单量、整手、最小价位等;all_instruments 按证券代码稳定排序返回全量证券。".to_string() },
|
||||||
ManualFunction { name: "active_instruments/instruments_history".to_string(), signature: "ctx.active_instruments(&[symbol])".to_string(), detail: "active_instruments 返回当前交易日已上市且未退市的证券;instruments_history 返回给定代码的历史证券记录,包含当前已退市标的,对齐 平台内核 的 active_instruments/instruments_history 能力。".to_string() },
|
ManualFunction { name: "active_instruments/instruments_history".to_string(), signature: "ctx.active_instruments(&[symbol])".to_string(), detail: "active_instruments 返回当前信号日已上市且未退市的证券;instruments_history 返回给定代码的历史证券记录,包含当前已退市标的,对齐 平台内核 的 active_instruments/instruments_history 能力。".to_string() },
|
||||||
ManualFunction { name: "get_trading_dates/get_previous_trading_date/get_next_trading_date".to_string(), signature: "ctx.get_previous_trading_date(date, n)".to_string(), detail: "交易日历 API。get_trading_dates 返回闭区间交易日;previous/next 返回相对某日向前或向后的第 n 个交易日,当前日自身不计入。".to_string() },
|
ManualFunction { name: "get_trading_dates/get_previous_trading_date/get_next_trading_date".to_string(), signature: "ctx.get_previous_trading_date(date, n)".to_string(), detail: "交易日历 API。get_trading_dates 返回闭区间交易日;previous/next 返回相对某日向前或向后的第 n 个交易日,当前日自身不计入。".to_string() },
|
||||||
ManualFunction { name: "is_suspended/is_st_stock".to_string(), signature: "ctx.is_suspended(symbol, count)".to_string(), detail: "读取指定证券截至当前交易日最近 count 个交易日的停牌或 ST 标记,返回 bool 序列,顺序从旧到新;对应平台内核的 is_suspended/is_st_stock 数据能力。".to_string() },
|
ManualFunction { name: "is_suspended/is_st_stock".to_string(), signature: "ctx.is_suspended(symbol, count)".to_string(), detail: "读取指定证券截至当前信号日最近 count 个交易日的停牌或 ST 标记,返回 bool 序列,顺序从旧到新;对应平台内核的 is_suspended/is_st_stock 数据能力。执行日停牌或 ST 只能由撮合风控判断,不能在 next_bar_open 的 T 日提前固化。".to_string() },
|
||||||
ManualFunction { name: "get_price".to_string(), signature: "ctx.get_price(symbol, start_date, end_date, \"1d\" | \"1m\" | \"tick\")".to_string(), detail: "按日期区间读取统一 PriceBar 序列。日线返回 open/high/low/close/last/volume/盘口字段;分钟或 tick 返回按 timestamp 排序的 last/bid1/ask1/volume_delta/amount_delta 映射,便于服务层转成表格或前端明细。".to_string() },
|
ManualFunction { name: "get_price".to_string(), signature: "ctx.get_price(symbol, start_date, end_date, \"1d\" | \"1m\")".to_string(), detail: "按日期区间读取统一 PriceBar 序列。日线返回 open/high/low/close/last/volume/盘口字段;分钟线返回按 timestamp 排序的 last/bid1/ask1/volume_delta/amount_delta 映射,便于服务层转成表格或前端明细。".to_string() },
|
||||||
ManualFunction { name: "get_dividend / dividend_cash / has_dividend".to_string(), signature: "dividend_cash(lookback) / has_dividend(lookback)".to_string(), detail: "高级数据 风格分红 API。Rust Context 可用 ctx.get_dividend(symbol, start_date) 读取明细;平台表达式可用 dividend_cash(lookback) 汇总当前股票最近 N 个交易日现金分红,用 has_dividend(lookback) 判断是否发生分红,也支持 dividend_cash(\"600000.SH\", lookback)。".to_string() },
|
ManualFunction { name: "get_dividend / dividend_cash / has_dividend".to_string(), signature: "dividend_cash(lookback) / has_dividend(lookback)".to_string(), detail: "高级数据 风格分红 API。Rust Context 可用 ctx.get_dividend(symbol, start_date) 读取明细;平台表达式可用 dividend_cash(lookback) 汇总当前股票最近 N 个交易日现金分红,用 has_dividend(lookback) 判断是否发生分红,也支持 dividend_cash(\"600000.SH\", lookback)。".to_string() },
|
||||||
ManualFunction { name: "get_split / split_ratio / has_split".to_string(), signature: "split_ratio(lookback) / has_split(lookback)".to_string(), detail: "高级数据 风格拆分/送转 API。Rust Context 可用 ctx.get_split(symbol, start_date) 读取明细;平台表达式可用 split_ratio(lookback) 计算当前股票最近 N 个交易日累计拆分比例,has_split(lookback) 判断是否发生送转。".to_string() },
|
ManualFunction { name: "get_split / split_ratio / has_split".to_string(), signature: "split_ratio(lookback) / has_split(lookback)".to_string(), detail: "高级数据 风格拆分/送转 API。Rust Context 可用 ctx.get_split(symbol, start_date) 读取明细;平台表达式可用 split_ratio(lookback) 计算当前股票最近 N 个交易日累计拆分比例,has_split(lookback) 判断是否发生送转。".to_string() },
|
||||||
ManualFunction { name: "get_factor / factor_value".to_string(), signature: "factor_value(\"field\", lookback=1)".to_string(), detail: "数值因子 API。factor(\"field\") 读取当前股票当日因子;factor_value(\"field\", lookback) 会在最近 N 个交易日内取该字段最新数值,适合读取任意可用指标或自定义数值因子。Rust Context 可用 ctx.get_factor(symbol, start, end, field) 读取完整数值序列。".to_string() },
|
ManualFunction { name: "get_factor / factor_value".to_string(), signature: "factor_value(\"field\", lookback=1)".to_string(), detail: "数值因子 API。factor(\"field\") 读取当前股票当日因子;factor_value(\"field\", lookback) 会在最近 N 个交易日内取该字段最新数值,适合读取任意可用指标或自定义数值因子。Rust Context 可用 ctx.get_factor(symbol, start, end, field) 读取完整数值序列。".to_string() },
|
||||||
@@ -328,13 +366,13 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
|||||||
ManualFunction { name: "order/order_status/order_avg_price/order_transaction_cost".to_string(), signature: "ctx.order(order_id)".to_string(), detail: "按订单 id 查询运行时订单对象,支持已结束订单和当前挂单。返回字段包括 status、filled_quantity、unfilled_quantity、avg_price、transaction_cost、symbol、side、reason;可用便捷函数读取状态、成交均价和费用,对齐 平台内核 Order 的核心属性。".to_string() },
|
ManualFunction { name: "order/order_status/order_avg_price/order_transaction_cost".to_string(), signature: "ctx.order(order_id)".to_string(), detail: "按订单 id 查询运行时订单对象,支持已结束订单和当前挂单。返回字段包括 status、filled_quantity、unfilled_quantity、avg_price、transaction_cost、symbol、side、reason;可用便捷函数读取状态、成交均价和费用,对齐 平台内核 Order 的核心属性。".to_string() },
|
||||||
ManualFunction { name: "account/portfolio_view/accounts".to_string(), signature: "ctx.account()".to_string(), detail: "返回当前股票账户/组合运行时视图,字段包括 account_type、cash、available_cash、frozen_cash、market_value、total_value、unit_net_value、daily_pnl、daily_returns、total_returns、transaction_cost、trading_pnl、position_pnl 等;DSL 中同名字段可直接使用。也可用 ctx.stock_account()、ctx.account_by_type(\"STOCK\")、ctx.accounts() 按账户类型读取;当前股票回测路径不会把 FUTURE 虚假映射成 STOCK。".to_string() },
|
ManualFunction { name: "account/portfolio_view/accounts".to_string(), signature: "ctx.account()".to_string(), detail: "返回当前股票账户/组合运行时视图,字段包括 account_type、cash、available_cash、frozen_cash、market_value、total_value、unit_net_value、daily_pnl、daily_returns、total_returns、transaction_cost、trading_pnl、position_pnl 等;DSL 中同名字段可直接使用。也可用 ctx.stock_account()、ctx.account_by_type(\"STOCK\")、ctx.accounts() 按账户类型读取;当前股票回测路径不会把 FUTURE 虚假映射成 STOCK。".to_string() },
|
||||||
ManualFunction { name: "deposit_withdraw/finance_repay/management_fee".to_string(), signature: "account.deposit_withdraw(amount, receiving_days=0)".to_string(), detail: "策略账户资金动作。deposit_withdraw 正数入金、负数出金;receiving_days 大于 0 时按交易日延迟到账,并保持净值口径不把外部资金流当成收益。finance_repay 正数融资、负数还款,会同步维护 cash_liabilities。set_management_fee_rate 设置结算管理费率;普通策略可覆盖 management_fee(ctx, rate) 自定义计算器,对齐 平台内核 管理费回调能力。".to_string() },
|
ManualFunction { name: "deposit_withdraw/finance_repay/management_fee".to_string(), signature: "account.deposit_withdraw(amount, receiving_days=0)".to_string(), detail: "策略账户资金动作。deposit_withdraw 正数入金、负数出金;receiving_days 大于 0 时按交易日延迟到账,并保持净值口径不把外部资金流当成收益。finance_repay 正数融资、负数还款,会同步维护 cash_liabilities。set_management_fee_rate 设置结算管理费率;普通策略可覆盖 management_fee(ctx, rate) 自定义计算器,对齐 平台内核 管理费回调能力。".to_string() },
|
||||||
ManualFunction { name: "rolling_mean / sma / ma".to_string(), signature: "rolling_mean(\"field\", lookback) / ma(\"close\", 20)".to_string(), detail: "任意字段滚动均值,支持 close、volume、amount、turnover_ratio、effective_turnover_ratio、signal_open/signal_close、benchmark_open/benchmark_close 和所有数值型 extra_factors。个股 close 使用当前交易日前已完成收盘序列,volume 使用当前交易日前已完成成交量序列;历史窗口不足时在选股过滤和买入仓位表达式中按不通过/0 仓处理。".to_string() },
|
ManualFunction { name: "rolling_mean / sma / ma".to_string(), signature: "rolling_mean(\"field\", lookback) / ma(\"close\", 20)".to_string(), detail: "任意字段滚动均值,支持 close、volume、amount、turnover_ratio、effective_turnover_ratio、signal_open/signal_close、benchmark_open/benchmark_close 和所有数值型 extra_factors。第一个参数必须是字段名或字符串字段名,不能传嵌套表达式或另一个 helper 调用。个股 close 使用当前交易日前已完成收盘序列,volume 使用当前交易日前已完成成交量序列;历史窗口不足时在选股过滤和买入仓位表达式中按不通过/0 仓处理。".to_string() },
|
||||||
ManualFunction { name: "vma".to_string(), signature: "vma(60)".to_string(), detail: "rolling_mean(\"volume\", lookback) 的便捷别名,用于任意窗口成交量均线,例如 vma(5) < vma(60)。".to_string() },
|
ManualFunction { name: "vma".to_string(), signature: "vma(60)".to_string(), detail: "rolling_mean(\"volume\", lookback) 的便捷别名,用于任意窗口成交量均线,例如 vma(5) < vma(60)。".to_string() },
|
||||||
ManualFunction { name: "rolling_sum / rolling_min / rolling_max".to_string(), signature: "rolling_sum(\"volume\", 20)".to_string(), detail: "任意数值字段滚动求和、最小值、最大值。可用于量能收缩、区间高低点、资金活跃度等过滤或排序。".to_string() },
|
ManualFunction { name: "rolling_sum / rolling_min / rolling_max".to_string(), signature: "rolling_sum(\"volume\", 20)".to_string(), detail: "任意数值字段滚动求和、最小值、最大值。第一个参数必须是字段名或字符串字段名,不能传嵌套表达式或另一个 helper 调用。可用于量能收缩、区间高低点、资金活跃度等过滤或排序。".to_string() },
|
||||||
ManualFunction { name: "rolling_stddev / stddev / rolling_zscore / pct_change".to_string(), signature: "stddev(\"close\", 20) / pct_change(\"close\", 10)".to_string(), detail: "滚动标准差、最新值 Z 分数和区间涨跌幅。pct_change(field, n) 会读取 n+1 个窗口点并计算 latest / first - 1。".to_string() },
|
ManualFunction { name: "rolling_stddev / stddev / rolling_zscore / pct_change".to_string(), signature: "stddev(\"close\", 20) / pct_change(\"close\", 10)".to_string(), detail: "滚动标准差、最新值 Z 分数和区间涨跌幅。第一个参数必须是字段名或字符串字段名,不能传嵌套表达式或另一个 helper 调用;需要收益率波动时先使用已注册收益率字段或发布因子,不要写 rolling_stddev(pct_change(\"close\", 1), 20)。pct_change(field, n) 会读取 n+1 个窗口点并计算 latest / first - 1。".to_string() },
|
||||||
ManualFunction { name: "数据库指标因子".to_string(), signature: "factor_value(\"ths_valid_turnover_stock\", 1)".to_string(), detail: "stock_indicator_factors_v1 中的指标会进入 extra_factors,可用 factor(\"字段\")、factors[\"字段\"]、factor_value(\"字段\", lookback) 或 rolling_mean(\"字段\", n) 读取。市值类指标统一提供亿元口径别名 ths_market_value_stock、ths_market_value_stock_bn、ths_current_mv_stock、ths_current_mv_stock_bn,同时保留 raw 后缀原始值。".to_string() },
|
ManualFunction { name: "Source Lake 指标因子".to_string(), signature: "factor_value(\"ths_valid_turnover_stock\", 1)".to_string(), detail: "Strategy Factory Source Lake 中已完成 PIT/as-of 审计的 source rows 字段、已发布指标或因子 artifact 会进入 extra_factors,可用 factor(\"字段\")、factors[\"字段\"]、factor_value(\"字段\", lookback) 或 rolling_mean(\"字段\", n) 读取。市值类指标统一提供亿元口径别名 ths_market_value_stock、ths_market_value_stock_bn、ths_current_mv_stock、ths_current_mv_stock_bn,同时保留 raw 后缀原始值。".to_string() },
|
||||||
ManualFunction { name: "round/floor/ceil/abs/min/max/clamp".to_string(), signature: "round(x)".to_string(), detail: "常用数值函数。".to_string() },
|
ManualFunction { name: "round/floor/ceil/abs/min/max/clamp".to_string(), signature: "round(x)".to_string(), detail: "常用数值函数。".to_string() },
|
||||||
ManualFunction { name: "safe_div".to_string(), signature: "safe_div(lhs, rhs, fallback)".to_string(), detail: "安全除法。".to_string() },
|
ManualFunction { name: "safe_div".to_string(), signature: "safe_div(lhs, rhs) / safe_div(lhs, rhs, fallback)".to_string(), detail: "安全除法,两参数形式默认 fallback=0。".to_string() },
|
||||||
ManualFunction { name: "contains/starts_with/ends_with/lower/upper/trim/strlen".to_string(), signature: "starts_with(symbol, \"60\")".to_string(), detail: "字符串辅助函数。".to_string() },
|
ManualFunction { name: "contains/starts_with/ends_with/lower/upper/trim/strlen".to_string(), signature: "starts_with(symbol, \"60\")".to_string(), detail: "字符串辅助函数。".to_string() },
|
||||||
],
|
],
|
||||||
factor_sources: vec![
|
factor_sources: vec![
|
||||||
@@ -360,12 +398,12 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
|||||||
},
|
},
|
||||||
ManualFactorSource {
|
ManualFactorSource {
|
||||||
table: "盘口深度参数".to_string(),
|
table: "盘口深度参数".to_string(),
|
||||||
detail: "可选字段包括 date、symbol、timestamp、level、bid_price、bid_volume、ask_price、ask_volume。存在盘口深度时,期货 counterparty_offer / next_tick_best_counterparty 可按真实多档盘口逐档扫单;不存在时不会伪造 depth。".to_string(),
|
detail: "可选字段包括 date、symbol、timestamp、level、bid_price、bid_volume、ask_price、ask_volume。存在盘口深度时,期货 minute_best_counterparty 可按真实多档盘口逐档扫单;不存在时不会伪造 depth。".to_string(),
|
||||||
fields: vec![],
|
fields: vec![],
|
||||||
},
|
},
|
||||||
ManualFactorSource {
|
ManualFactorSource {
|
||||||
table: "期货交易参数".to_string(),
|
table: "期货交易参数".to_string(),
|
||||||
detail: "字段包括 symbol、effective_date、contract_multiplier、long_margin_rate、short_margin_rate、commission_type、open_commission_ratio、close_commission_ratio、close_today_commission_ratio、price_tick。回测会按交易日自动选择不晚于当前日期的最新参数,用于保证金、手续费和限价 tick 校验。".to_string(),
|
detail: "字段包括 symbol、effective_date、contract_multiplier、long_margin_rate、short_margin_rate、commission_type、open_commission_ratio、close_commission_ratio、close_today_commission_ratio、price_tick。回测会按交易日自动选择不晚于当前日期的最新参数,用于保证金、手续费和限价最小价位校验。".to_string(),
|
||||||
fields: vec![],
|
fields: vec![],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -383,8 +421,8 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
|||||||
code: "filter.stock_expr(industry_name(\"citics\", 1) == \"电子\" && factor_text(\"concept\") == \"ai_chip\")".to_string(),
|
code: "filter.stock_expr(industry_name(\"citics\", 1) == \"电子\" && factor_text(\"concept\") == \"ai_chip\")".to_string(),
|
||||||
},
|
},
|
||||||
ManualExample {
|
ManualExample {
|
||||||
title: "next tick 撮合 + tick 滑点".to_string(),
|
title: "分钟执行价撮合 + 最小价位滑点".to_string(),
|
||||||
code: "execution.matching_type(\"next_tick_last\")\nexecution.slippage(\"tick_size\", 1)".to_string(),
|
code: "execution.matching_type(\"minute_last\")\nexecution.slippage(\"tick_size\", 1)".to_string(),
|
||||||
},
|
},
|
||||||
ManualExample {
|
ManualExample {
|
||||||
title: "动态 universe 和订阅".to_string(),
|
title: "动态 universe 和订阅".to_string(),
|
||||||
@@ -432,17 +470,21 @@ pub fn render_manual_markdown(manual: &StrategyAiManual) -> String {
|
|||||||
out.push_str("## AI 代码生成硬约束\n");
|
out.push_str("## AI 代码生成硬约束\n");
|
||||||
out.push_str("- 只输出完整 `engine-script` 代码;第一行必须是 `strategy(\"...\")`、`let`、`fn`、`const` 或 `//`。\n");
|
out.push_str("- 只输出完整 `engine-script` 代码;第一行必须是 `strategy(\"...\")`、`let`、`fn`、`const` 或 `//`。\n");
|
||||||
out.push_str("- 禁止输出 Markdown、解释、推理过程、JSON 包装、手册复述或结果报告。\n");
|
out.push_str("- 禁止输出 Markdown、解释、推理过程、JSON 包装、手册复述或结果报告。\n");
|
||||||
out.push_str("- 只使用支持语句块:`market`、`benchmark`、`signal`、`rebalance.every_days(...).at([...])`、`selection.limit`、`selection.market_cap_band`、`filter.stock_ma`、`filter.stock_expr`、`ordering.rank_by`、`ordering.rank_expr`、`allocation.buy_scale`、`risk.stop_loss`、`risk.take_profit`、`risk.index_exposure`、`execution.matching_type`、`execution.slippage`、`universe.exclude`。\n");
|
out.push_str("- 只使用支持语句块:`market`、`benchmark`、`signal`、`rebalance.every_days(...).at([...])`、`selection.limit`、`selection.market_cap_band`、`filter.stock_ma`、`filter.stock_expr`、`ordering.rank_by`、`ordering.rank_expr`、`allocation.buy_scale`、`risk.stop_loss`、`risk.take_profit`、`risk.index_exposure`、`risk.policy`、`risk.blacklist`、`execution.matching_type`、`execution.rebalance_cash_mode`、`execution.slippage`、`universe.exclude`。\n");
|
||||||
|
out.push_str("- `universe.exclude` 只用于用户明确要求的业务排除项;ST、停牌、退市、新股、科创、一元、涨跌停、同日卖出禁买、成交量、手续费和印花税等基础风控必须写 `risk.policy(...)` 或由运行态 RiskLimits 注入。\n");
|
||||||
out.push_str("- 禁止伪 DSL:`filter(...)`、`rank(...)`、`select.top(...)`、`weight.equal(...)`、`sell_rule(...)`、`backtest(...)`、`risk.max_position(...)`。\n");
|
out.push_str("- 禁止伪 DSL:`filter(...)`、`rank(...)`、`select.top(...)`、`weight.equal(...)`、`sell_rule(...)`、`backtest(...)`、`risk.max_position(...)`。\n");
|
||||||
out.push_str("- 市值表达式字段只能用 `market_cap` 或 `free_float_cap`;不要使用数据库原始字段 `float_market_cap`。\n");
|
out.push_str("- 市值表达式字段只能用 `market_cap` 或 `free_float_cap`;不要使用数据库原始字段 `float_market_cap`。\n");
|
||||||
out.push_str("- 任意窗口价格均线使用 `rolling_mean(\"close\", n)` 或 `ma(\"close\", n)`;任意窗口均量使用 `rolling_mean(\"volume\", n)` 或 `vma(n)`;不要使用未列出的 `ma60`、`stock_ma60`、`signal_ma60` 或 `benchmark_ma60` 变量。\n");
|
out.push_str("- 任意窗口价格均线使用 `rolling_mean(\"close\", n)` 或 `ma(\"close\", n)`;任意窗口均量使用 `rolling_mean(\"volume\", n)` 或 `vma(n)`;不要使用未列出的 `ma60`、`stock_ma60`、`signal_ma60` 或 `benchmark_ma60` 变量。\n");
|
||||||
|
out.push_str("- `rolling_mean`、`rolling_sum/min/max/stddev/zscore`、`pct_change`、`factor_value` 等 helper 的第一个参数必须是字段名或字符串字段名;不要输出 `rolling_stddev(pct_change(\"close\", 1), 20)` 这类嵌套表达式。\n");
|
||||||
out.push_str("- 自定义 `fn` 必须通过参数传入运行时字段;不要用 `fn score()` 这类零参数函数直接引用 `market_cap`、`close`、`ma5` 等股票字段。\n");
|
out.push_str("- 自定义 `fn` 必须通过参数传入运行时字段;不要用 `fn score()` 这类零参数函数直接引用 `market_cap`、`close`、`ma5` 等股票字段。\n");
|
||||||
out.push_str("- `selection.market_cap_band` 必须写命名参数:`field=\"market_cap\"` 或 `field=\"free_float_cap\"`,并包含 `lower=...` 与 `upper=...`。\n");
|
out.push_str("- `selection.market_cap_band` 必须写命名参数:`field=\"market_cap\"` 或 `field=\"free_float_cap\"`,并包含 `lower=...` 与 `upper=...`。\n");
|
||||||
out.push_str(
|
out.push_str(
|
||||||
"- `risk.index_exposure(...)` 只能传一个表达式;不要生成 `risk.exposure(...)`。\n",
|
"- `risk.index_exposure(...)` 只能传一个表达式;不要生成 `risk.exposure(...)`。\n",
|
||||||
);
|
);
|
||||||
|
out.push_str("- `filter.stock_expr(...)` 只写 alpha 或业务过滤条件;不要把 `!is_st`、`!paused`、`!at_upper_limit`、`!at_lower_limit` 这类基础风控散落在过滤表达式里。\n");
|
||||||
out.push_str("- 完整三元表达式 `cond ? a : b` 可在表达式参数中使用;若当前运行环境报 `Unknown operator: '?'`,先重编译并重启回测服务,不要改写策略语义掩盖运行时漂移。\n");
|
out.push_str("- 完整三元表达式 `cond ? a : b` 可在表达式参数中使用;若当前运行环境报 `Unknown operator: '?'`,先重编译并重启回测服务,不要改写策略语义掩盖运行时漂移。\n");
|
||||||
out.push_str("- `next_bar_open` 的选股、排序和仓位信号来自决策日,订单在下一可交易开盘撮合;不要使用执行日价格作为下单前信号。\n");
|
out.push_str("- `next_bar_open` 的选股、排序和仓位信号来自决策日,订单在下一可交易开盘撮合;不要使用执行日价格作为下单前信号。\n");
|
||||||
|
out.push_str("- `next_bar_open` 必须区分信号日、订单创建日和实际成交日:T 日只生成订单意图,涨跌停、停牌、ST、退市、一元股、黑名单、成交量和盘口流动性等执行约束必须由撮合/风控层按实际成交日判断;涨停买入和跌停卖出必须比较实际 next-open 成交价与涨跌停价,不能用执行日 close/last 或 next-close;禁止用 T 日执行状态拦截 T+1 可交易订单。\n");
|
||||||
out.push_str("- `execution.matching_type(...)` 和 `execution.slippage(...)` 必须使用手册列出的合法取值。\n\n");
|
out.push_str("- `execution.matching_type(...)` 和 `execution.slippage(...)` 必须使用手册列出的合法取值。\n\n");
|
||||||
out.push_str("## 语句块\n");
|
out.push_str("## 语句块\n");
|
||||||
for item in &manual.statement_blocks {
|
for item in &manual.statement_blocks {
|
||||||
@@ -514,13 +556,19 @@ pub fn build_generation_prompt(
|
|||||||
prompt.push_str("- 不要输出解释文本。\n");
|
prompt.push_str("- 不要输出解释文本。\n");
|
||||||
prompt.push_str("- 必须使用 strategy(\"...\") { ... } 语法。\n");
|
prompt.push_str("- 必须使用 strategy(\"...\") { ... } 语法。\n");
|
||||||
prompt.push_str("- 如需自定义参数,使用 let 和 fn。\n");
|
prompt.push_str("- 如需自定义参数,使用 let 和 fn。\n");
|
||||||
prompt.push_str("- 优先使用数据库已存在字段和 factors[...]。\n\n");
|
prompt.push_str("- 优先使用 Strategy Factory Source Lake 已注册 source rows 字段、已发布指标/因子 artifact 和运行时已存在字段、factors[...];不要回退 ficlaw-data、QuantAPI、旧数据中心 HTTP、ClickHouse 或临时文件。\n\n");
|
||||||
prompt.push_str("- 生成的代码必须能转换为 strategy_spec 并提交 POST /v1/backtests。\n");
|
prompt.push_str("- 生成的代码必须能转换为 strategy_spec 并提交 POST /v1/backtests。\n");
|
||||||
|
prompt.push_str("- 用户指定“持仓N只、目标持仓N、stocknum=N、selection.limit(N)”时,必须把最终持仓槽位写成 N;用户指定“至少/不少于N只”时,最终持仓槽位必须 >= N。\n");
|
||||||
|
prompt.push_str("- ");
|
||||||
|
prompt.push_str(DEFAULT_THREE_YEAR_RETURN_TARGET_PROMPT);
|
||||||
|
prompt.push('\n');
|
||||||
prompt.push_str("- 不要使用手册未列出的字段、函数或外部平台 API 名称。\n\n");
|
prompt.push_str("- 不要使用手册未列出的字段、函数或外部平台 API 名称。\n\n");
|
||||||
prompt.push_str("只允许使用这些可编译语句:market、benchmark、signal、rebalance.every_days(...).at([...])、selection.limit、selection.market_cap_band、filter.stock_ma、filter.stock_expr、ordering.rank_by、ordering.rank_expr、allocation.buy_scale、risk.stop_loss、risk.take_profit、risk.index_exposure、execution.matching_type、execution.slippage、universe.exclude。禁止输出 filter(...)、rank(...)、select.top(...)、weight.equal()、sell_rule(...)、backtest(...)、risk.max_position(...) 这类未支持伪语法。\n");
|
prompt.push_str("只允许使用这些可编译语句:market、benchmark、signal、rebalance.every_days(...).at([...])、selection.limit、selection.market_cap_band、filter.stock_ma、filter.stock_expr、ordering.rank_by、ordering.rank_expr、allocation.buy_scale、risk.stop_loss、risk.take_profit、risk.index_exposure、risk.policy、risk.blacklist、execution.matching_type、execution.slippage、universe.exclude。universe.exclude 只用于用户明确要求的业务排除项,不能表达 FIDC 基础风控。禁止输出 filter(...)、rank(...)、select.top(...)、weight.equal()、sell_rule(...)、backtest(...)、risk.max_position(...) 这类未支持伪语法。\n");
|
||||||
prompt.push_str("参数形态必须严格:selection.market_cap_band 必须写 field=\"market_cap\" 或 field=\"free_float_cap\", lower=..., upper=...;禁止使用 float_market_cap;禁止使用 ma60、stock_ma60、signal_ma60、benchmark_ma60,60日价格均线写 rolling_mean(\"close\", 60) 或 ma(\"close\", 60),任意窗口均量写 rolling_mean(\"volume\", n) 或 vma(n);不要生成 fn score() 这类零参数函数,股票字段排序直接写在 ordering.rank_expr 内或用带参数函数;布尔字段按布尔使用,写 !is_st、!paused、!at_upper_limit、!at_lower_limit,不要写 is_st == 0;risk.index_exposure 只能传一个数值表达式,不要使用 risk.exposure;完整三元表达式 cond ? a : b 可以使用,但不得输出残缺问号/冒号片段;execution.matching_type 只能取 next_tick_last、next_tick_best_own、next_tick_best_counterparty、counterparty_offer、vwap、current_bar_close、next_bar_open、open_auction;next_bar_open 只能使用决策日信号,不能把执行日价格当作下单前信息;execution.slippage 必须写 execution.slippage(\"none\") 或 execution.slippage(\"price_ratio\", 0.001)。\n");
|
prompt.push_str(&format!("参数形态必须严格:selection.market_cap_band 必须写 field=\"market_cap\" 或 field=\"free_float_cap\", lower=..., upper=...;禁止使用 float_market_cap;禁止使用 ma60、stock_ma60、signal_ma60、benchmark_ma60,60日价格均线写 rolling_mean(\"close\", 60) 或 ma(\"close\", 60),任意窗口均量写 rolling_mean(\"volume\", n) 或 vma(n);rolling_mean、rolling_sum/min/max/stddev/zscore、pct_change、factor_value 等 helper 的第一个参数必须是字段名或字符串字段名,不能传嵌套表达式或另一个 helper 调用;不要生成 fn score() 这类零参数函数,股票字段排序直接写在 ordering.rank_expr 内或用带参数函数;布尔字段按布尔使用,不要写 is_st == 0;filter.stock_expr 只写 alpha 或业务过滤条件,不要把 !is_st、!paused、!at_upper_limit、!at_lower_limit 这类基础风控散落在表达式里;risk.index_exposure 只能传一个数值表达式,不要使用 risk.exposure;risk.policy 只写 FIDC 基础风控、成交量和交易成本命名参数,必须覆盖完整默认配置面,例如 {DEFAULT_RISK_POLICY_DSL_PROMPT},不要用它表达策略择时或收益规则;完整三元表达式 cond ? a : b 可以使用,但不得输出残缺问号/冒号片段;日线回测 execution.matching_type 只能取 current_bar_close 或 next_bar_open,分钟线回测只能取 minute_last;不要把 vwap、twap、open_auction、minute_best_own、minute_best_counterparty 写成全局 matching_type;next_bar_open 只能使用决策日信号,不能把执行日价格当作下单前信息;next_bar_open 下 T 日只生成订单意图,涨跌停、停牌、ST、退市、一元股、黑名单、成交量和盘口流动性等执行约束必须由撮合/风控层按实际成交日判断;涨停买入和跌停卖出必须用实际 next-open 成交价比较,不能用执行日 close/last 或 next-close;禁止用 T 日执行状态拦截 T+1 可交易订单;execution.slippage 必须写 execution.slippage(\"none\") 或 execution.slippage(\"price_ratio\", 0.001)。\n"));
|
||||||
prompt.push_str("回测成功但 tradeCount=0 或 holdingCount=0 是无效策略;第一版必须保持稳定买入覆盖率,复杂因子只能在后续优化中逐步加严。\n");
|
prompt.push_str("回测成功但 tradeCount=0 或 holdingCount=0 是无效策略;第一版必须保持稳定买入覆盖率,复杂因子只能在后续优化中逐步加严。\n");
|
||||||
prompt.push_str("可参考但不要照抄的最小模板,回复时不要包含 ``` 代码围栏:\nstrategy(\"cn_a_smallcap_factor_rotation\") {\nmarket(\"CN_A\")\nbenchmark(\"000852.SH\")\nsignal(\"000001.SH\")\nrebalance.every_days(5).at([\"10:18\"])\nselection.limit(40)\nselection.market_cap_band(field=\"market_cap\", lower=0, upper=1000)\nfilter.stock_expr(listed_days >= 60 && !is_st && !paused && close > 2 && !at_upper_limit && !at_lower_limit)\nordering.rank_by(\"market_cap\", \"asc\")\nallocation.buy_scale(1.0)\nrisk.index_exposure(1.0)\nrisk.stop_loss(holding_return < -0.08)\nexecution.slippage(\"price_ratio\", 0.001)\n}\n\n");
|
prompt.push_str("可参考但不要照抄的最小模板,回复时不要包含 ``` 代码围栏:\nstrategy(\"cn_a_smallcap_factor_rotation\") {\nmarket(\"CN_A\")\nbenchmark(\"000852.SH\")\nsignal(\"000001.SH\")\nrebalance.every_days(5).at([\"10:18\"])\nselection.limit(40)\nselection.market_cap_band(field=\"market_cap\", lower=0, upper=1000)\nfilter.stock_expr(listed_days >= 60 && close > 2)\nordering.rank_by(\"market_cap\", \"asc\")\nallocation.buy_scale(1.0)\nrisk.policy(");
|
||||||
|
prompt.push_str(DEFAULT_RISK_POLICY_DSL_CODE);
|
||||||
|
prompt.push_str(")\nrisk.index_exposure(1.0)\nrisk.stop_loss(holding_return < -0.08)\nexecution.slippage(\"price_ratio\", 0.001)\n}\n\n");
|
||||||
prompt.push_str("用户目标:\n");
|
prompt.push_str("用户目标:\n");
|
||||||
prompt.push_str(&format!("- {}\n", request.user_goal));
|
prompt.push_str(&format!("- {}\n", request.user_goal));
|
||||||
if !request.constraints.is_empty() {
|
if !request.constraints.is_empty() {
|
||||||
@@ -547,7 +595,10 @@ pub fn build_optimization_prompt(
|
|||||||
prompt.push_str("输出格式硬约束:回复第一行必须是 strategy(\"...\")、let、fn、const 或 //;回复中不得包含 Markdown、解释、思考过程、手册复述、JSON 包装或自然语言总结。\n");
|
prompt.push_str("输出格式硬约束:回复第一行必须是 strategy(\"...\")、let、fn、const 或 //;回复中不得包含 Markdown、解释、思考过程、手册复述、JSON 包装或自然语言总结。\n");
|
||||||
prompt.push_str("长度硬约束:策略代码目标 80 行以内,只保留必要 let/fn/strategy 块;不要复制下面的手册片段、历史策略全文或字段清单。\n");
|
prompt.push_str("长度硬约束:策略代码目标 80 行以内,只保留必要 let/fn/strategy 块;不要复制下面的手册片段、历史策略全文或字段清单。\n");
|
||||||
prompt.push_str("优化不限制在原策略已有参数或少量扰动。只要 OmniQuant/FIDC 已支持,可以自由增加、修改、删除策略代码、参数、候选池、过滤函数、排序、仓位、止盈止损、调仓周期、指标因子和辅助函数;不得引入手册未列出的字段或外部平台 API 名称。\n");
|
prompt.push_str("优化不限制在原策略已有参数或少量扰动。只要 OmniQuant/FIDC 已支持,可以自由增加、修改、删除策略代码、参数、候选池、过滤函数、排序、仓位、止盈止损、调仓周期、指标因子和辅助函数;不得引入手册未列出的字段或外部平台 API 名称。\n");
|
||||||
prompt.push_str("可以使用所有已入库日频字段、指标因子和表达式函数,例如 rolling_mean/ma/vma/rolling_sum/rolling_stddev/pct_change/factor/factor_value/factors;如上一轮无交易或质量分过低,必须先扩大候选覆盖并修正不可交易过滤,再优化收益。\n");
|
prompt.push_str("持仓数量属于策略合同,不是优化自由参数。原策略或用户目标明确 stocknum、selection.limit、目标持仓N只或不少于N只时,优化后必须保留该目标槽位或满足最低槽位,不能为了收益或交易次数擅自改小。\n");
|
||||||
|
prompt.push_str(DEFAULT_THREE_YEAR_RETURN_TARGET_PROMPT);
|
||||||
|
prompt.push('\n');
|
||||||
|
prompt.push_str("可以使用 Strategy Factory Source Lake 已注册并完成 PIT/as-of 审计的日频 source rows 字段、已发布指标/因子 artifact 和表达式函数,例如 rolling_mean/ma/vma/rolling_sum/rolling_stddev/pct_change/factor/factor_value/factors;这些滚动/因子 helper 的字段参数只能是字段名或字符串字段名,不要嵌套表达式;不要回退 ficlaw-data、QuantAPI、旧数据中心 HTTP、ClickHouse 或临时文件。如上一轮无交易或质量分过低,必须先扩大候选覆盖并修正不可交易过滤,再优化收益。\n");
|
||||||
prompt.push_str("优化目标:\n");
|
prompt.push_str("优化目标:\n");
|
||||||
prompt.push_str(&format!("- {}\n\n", request.objective));
|
prompt.push_str(&format!("- {}\n\n", request.objective));
|
||||||
prompt.push_str("当前策略代码如下,仅作为输入参考;回复时不要包含 Markdown 代码围栏:\n");
|
prompt.push_str("当前策略代码如下,仅作为输入参考;回复时不要包含 Markdown 代码围栏:\n");
|
||||||
@@ -569,3 +620,63 @@ pub fn build_optimization_prompt(
|
|||||||
prompt.push_str(manual_markdown);
|
prompt.push_str(manual_markdown);
|
||||||
prompt
|
prompt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generation_prompt_contains_three_year_return_acceptance_target() {
|
||||||
|
let prompt = build_generation_prompt(
|
||||||
|
"manual",
|
||||||
|
&StrategyAiGenerateRequest {
|
||||||
|
user_goal: "生成策略".to_string(),
|
||||||
|
constraints: Vec::new(),
|
||||||
|
market: "CN_A".to_string(),
|
||||||
|
benchmark_symbol: "000852.SH".to_string(),
|
||||||
|
signal_symbol: "000001.SH".to_string(),
|
||||||
|
holding_count_contract: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(prompt.contains("三年回测区间策略总收益严格 > 120% 才视为满足收益目标"));
|
||||||
|
assert!(prompt.contains("不得把已达标策略判为失败"));
|
||||||
|
assert!(prompt.contains("Strategy Factory Source Lake 已注册 source rows 字段"));
|
||||||
|
assert!(prompt.contains("不要回退 ficlaw-data"));
|
||||||
|
assert!(prompt.contains("ClickHouse"));
|
||||||
|
assert!(prompt.contains("T 日只生成订单意图"));
|
||||||
|
assert!(prompt.contains("按实际成交日判断"));
|
||||||
|
assert!(prompt.contains("禁止用 T 日执行状态拦截 T+1 可交易订单"));
|
||||||
|
assert!(prompt.contains("必须覆盖完整默认配置面"));
|
||||||
|
assert!(prompt.contains("reject_inactive_buy=true"));
|
||||||
|
assert!(prompt.contains("reject_inactive_sell=true"));
|
||||||
|
assert!(prompt.contains("reject_new_listing_buy=true"));
|
||||||
|
assert!(prompt.contains("reject_kcb_buy=true"));
|
||||||
|
assert!(prompt.contains("reject_bjse_buy=true"));
|
||||||
|
assert!(prompt.contains("reject_one_yuan_buy=true"));
|
||||||
|
assert!(prompt.contains("respect_allow_buy_sell=true"));
|
||||||
|
assert!(prompt.contains("stamp_tax_rate_before_change=0.001"));
|
||||||
|
assert!(prompt.contains("stamp_tax_change_date=\"2023-08-28\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn optimization_prompt_contains_three_year_return_acceptance_target() {
|
||||||
|
let prompt = build_optimization_prompt(
|
||||||
|
"manual",
|
||||||
|
&StrategyAiOptimizeRequest {
|
||||||
|
current_code: "strategy(\"demo\") {}".to_string(),
|
||||||
|
objective: "优化收益".to_string(),
|
||||||
|
result_summary: json!({ "total_return": 1.49 }),
|
||||||
|
diagnostics: Vec::new(),
|
||||||
|
holding_count_contract: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(prompt.contains("三年回测区间策略总收益严格 > 120% 才视为满足收益目标"));
|
||||||
|
assert!(prompt.contains("继续优化夏普、回撤、换手和稳定性"));
|
||||||
|
assert!(prompt.contains("Strategy Factory Source Lake 已注册并完成 PIT/as-of 审计"));
|
||||||
|
assert!(prompt.contains("不要回退 ficlaw-data"));
|
||||||
|
assert!(prompt.contains("ClickHouse"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use chrono::NaiveDate;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::data::{BenchmarkSnapshot, DataSet, EligibleUniverseSnapshot};
|
use crate::data::{BenchmarkSnapshot, DataSet, EligibleUniverseSnapshot};
|
||||||
|
use crate::risk_control::{ChinaAShareRiskControl, FidcRiskControlConfig, FidcRiskDecisionAudit};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum BandRegime {
|
pub enum BandRegime {
|
||||||
@@ -39,6 +40,7 @@ pub struct SelectionDiagnostics {
|
|||||||
pub missing_market_cap_symbols: Vec<String>,
|
pub missing_market_cap_symbols: Vec<String>,
|
||||||
pub selected_symbols: Vec<String>,
|
pub selected_symbols: Vec<String>,
|
||||||
pub rejection_examples: Vec<String>,
|
pub rejection_examples: Vec<String>,
|
||||||
|
pub risk_decisions: Vec<FidcRiskDecisionAudit>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SelectionContext<'a> {
|
pub struct SelectionContext<'a> {
|
||||||
@@ -47,20 +49,62 @@ pub struct SelectionContext<'a> {
|
|||||||
pub reference_level: f64,
|
pub reference_level: f64,
|
||||||
pub data: &'a DataSet,
|
pub data: &'a DataSet,
|
||||||
pub dynamic_universe: Option<&'a BTreeSet<String>>,
|
pub dynamic_universe: Option<&'a BTreeSet<String>>,
|
||||||
|
pub risk_config: Option<&'a FidcRiskControlConfig>,
|
||||||
|
pub defer_selection_risk: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SelectionContext<'_> {
|
impl SelectionContext<'_> {
|
||||||
fn eligible_universe(&self) -> Vec<EligibleUniverseSnapshot> {
|
fn eligible_universe(&self) -> Vec<EligibleUniverseSnapshot> {
|
||||||
let eligible = self.data.eligible_universe_on(self.decision_date);
|
let eligible = match (self.risk_config, self.defer_selection_risk) {
|
||||||
|
(Some(risk_config), false) => self
|
||||||
|
.data
|
||||||
|
.eligible_universe_on_with_risk_config(self.decision_date, risk_config),
|
||||||
|
_ => self.data.eligible_universe_on(self.decision_date).to_vec(),
|
||||||
|
};
|
||||||
match self.dynamic_universe {
|
match self.dynamic_universe {
|
||||||
Some(symbols) if !symbols.is_empty() => eligible
|
Some(symbols) if !symbols.is_empty() => eligible
|
||||||
.iter()
|
.into_iter()
|
||||||
.filter(|row| symbols.contains(&row.symbol))
|
.filter(|row| symbols.contains(&row.symbol))
|
||||||
.cloned()
|
|
||||||
.collect(),
|
.collect(),
|
||||||
_ => eligible.to_vec(),
|
_ => eligible,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn selection_risk_decisions(&self) -> Vec<FidcRiskDecisionAudit> {
|
||||||
|
let default_risk_config;
|
||||||
|
let risk_config = match self.risk_config {
|
||||||
|
Some(value) => value,
|
||||||
|
None => {
|
||||||
|
default_risk_config = FidcRiskControlConfig::default();
|
||||||
|
&default_risk_config
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut decisions = Vec::new();
|
||||||
|
for factor in self.data.factor_snapshots_on(self.decision_date) {
|
||||||
|
if self
|
||||||
|
.dynamic_universe
|
||||||
|
.is_some_and(|symbols| !symbols.is_empty() && !symbols.contains(&factor.symbol))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(candidate) = self.data.candidate(self.decision_date, &factor.symbol) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(market) = self.data.market(self.decision_date, &factor.symbol) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if let Some(decision) = ChinaAShareRiskControl::selection_rejection_decision_with_config(
|
||||||
|
self.decision_date,
|
||||||
|
candidate,
|
||||||
|
market,
|
||||||
|
self.data.instrument(&factor.symbol),
|
||||||
|
risk_config,
|
||||||
|
) {
|
||||||
|
decisions.push(decision);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
decisions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait UniverseSelector {
|
pub trait UniverseSelector {
|
||||||
@@ -166,9 +210,23 @@ impl UniverseSelector for DynamicMarketCapBandSelector {
|
|||||||
missing_market_cap_symbols: Vec::new(),
|
missing_market_cap_symbols: Vec::new(),
|
||||||
selected_symbols: Vec::new(),
|
selected_symbols: Vec::new(),
|
||||||
rejection_examples: Vec::new(),
|
rejection_examples: Vec::new(),
|
||||||
|
risk_decisions: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
diagnostics.factor_total = ctx.data.factor_snapshots_on(ctx.decision_date).len();
|
diagnostics.factor_total = ctx.data.factor_snapshots_on(ctx.decision_date).len();
|
||||||
|
diagnostics.risk_decisions = ctx.selection_risk_decisions();
|
||||||
|
diagnostics.not_eligible_count = diagnostics.risk_decisions.len();
|
||||||
|
diagnostics.paused_count = diagnostics
|
||||||
|
.risk_decisions
|
||||||
|
.iter()
|
||||||
|
.filter(|decision| decision.rule_code == "paused")
|
||||||
|
.count();
|
||||||
|
diagnostics.rejection_examples = diagnostics
|
||||||
|
.risk_decisions
|
||||||
|
.iter()
|
||||||
|
.take(8)
|
||||||
|
.map(|decision| format!("{} rejected by {}", decision.symbol, decision.rule_code))
|
||||||
|
.collect();
|
||||||
let eligible = ctx.eligible_universe();
|
let eligible = ctx.eligible_universe();
|
||||||
diagnostics.market_cap_missing_count =
|
diagnostics.market_cap_missing_count =
|
||||||
diagnostics.factor_total.saturating_sub(eligible.len());
|
diagnostics.factor_total.saturating_sub(eligible.len());
|
||||||
@@ -221,3 +279,266 @@ fn to_universe_candidate(
|
|||||||
band_high,
|
band_high,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::data::{
|
||||||
|
BenchmarkSnapshot, CandidateEligibility, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
|
||||||
|
};
|
||||||
|
use crate::instrument::Instrument;
|
||||||
|
|
||||||
|
fn d() -> NaiveDate {
|
||||||
|
NaiveDate::from_ymd_opt(2025, 1, 2).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instrument(symbol: &str) -> Instrument {
|
||||||
|
Instrument {
|
||||||
|
symbol: symbol.to_string(),
|
||||||
|
name: symbol.to_string(),
|
||||||
|
board: symbol.rsplit('.').next().unwrap_or("").to_string(),
|
||||||
|
round_lot: 100,
|
||||||
|
listed_at: Some(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()),
|
||||||
|
delisted_at: None,
|
||||||
|
status: "active".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn market(symbol: &str, price: f64) -> DailyMarketSnapshot {
|
||||||
|
DailyMarketSnapshot {
|
||||||
|
date: d(),
|
||||||
|
symbol: symbol.to_string(),
|
||||||
|
timestamp: Some("2025-01-02 10:00:00".to_string()),
|
||||||
|
day_open: price,
|
||||||
|
open: price,
|
||||||
|
high: price,
|
||||||
|
low: price,
|
||||||
|
close: price,
|
||||||
|
last_price: price,
|
||||||
|
bid1: price,
|
||||||
|
ask1: price,
|
||||||
|
prev_close: price,
|
||||||
|
volume: 1_000_000,
|
||||||
|
minute_volume: 10_000,
|
||||||
|
bid1_volume: 10_000,
|
||||||
|
ask1_volume: 10_000,
|
||||||
|
trading_phase: Some("continuous".to_string()),
|
||||||
|
paused: false,
|
||||||
|
upper_limit: price * 1.1,
|
||||||
|
lower_limit: price * 0.9,
|
||||||
|
price_tick: 0.01,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn factor(symbol: &str, market_cap_bn: f64) -> DailyFactorSnapshot {
|
||||||
|
DailyFactorSnapshot {
|
||||||
|
date: d(),
|
||||||
|
symbol: symbol.to_string(),
|
||||||
|
market_cap_bn,
|
||||||
|
free_float_cap_bn: market_cap_bn,
|
||||||
|
pe_ttm: 10.0,
|
||||||
|
turnover_ratio: Some(0.01),
|
||||||
|
effective_turnover_ratio: Some(0.01),
|
||||||
|
extra_factors: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn candidate(symbol: &str, is_st: bool, is_kcb: bool) -> CandidateEligibility {
|
||||||
|
CandidateEligibility {
|
||||||
|
date: d(),
|
||||||
|
symbol: symbol.to_string(),
|
||||||
|
is_st,
|
||||||
|
is_star_st: false,
|
||||||
|
is_new_listing: false,
|
||||||
|
is_paused: false,
|
||||||
|
allow_buy: true,
|
||||||
|
allow_sell: true,
|
||||||
|
is_kcb,
|
||||||
|
is_one_yuan: false,
|
||||||
|
risk_level_code: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn benchmark() -> BenchmarkSnapshot {
|
||||||
|
BenchmarkSnapshot {
|
||||||
|
date: d(),
|
||||||
|
benchmark: "000852.SH".to_string(),
|
||||||
|
open: 2000.0,
|
||||||
|
close: 2000.0,
|
||||||
|
prev_close: 1990.0,
|
||||||
|
volume: 1_000_000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn selector_records_structured_selection_risk_decisions() {
|
||||||
|
let data = DataSet::from_components(
|
||||||
|
vec![
|
||||||
|
instrument("000001.SZ"),
|
||||||
|
instrument("688001.SH"),
|
||||||
|
instrument("000002.SZ"),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
market("000001.SZ", 10.0),
|
||||||
|
market("688001.SH", 10.0),
|
||||||
|
market("000002.SZ", 10.0),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
factor("000001.SZ", 8.0),
|
||||||
|
factor("688001.SH", 9.0),
|
||||||
|
factor("000002.SZ", 10.0),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
candidate("000001.SZ", true, false),
|
||||||
|
candidate("688001.SH", false, true),
|
||||||
|
candidate("000002.SZ", false, false),
|
||||||
|
],
|
||||||
|
vec![benchmark()],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let selector = DynamicMarketCapBandSelector::new(2000.0, 7.0, 10.0, 0.0, 10, 0.0, 0.0, 0.0);
|
||||||
|
let mut risk_config = FidcRiskControlConfig::default();
|
||||||
|
risk_config.static_rules.reject_st_selection = true;
|
||||||
|
risk_config.static_rules.reject_kcb_selection = true;
|
||||||
|
let (_selected, diagnostics) = selector.select_with_diagnostics(&SelectionContext {
|
||||||
|
decision_date: d(),
|
||||||
|
benchmark: &benchmark(),
|
||||||
|
reference_level: 2000.0,
|
||||||
|
data: &data,
|
||||||
|
dynamic_universe: None,
|
||||||
|
risk_config: Some(&risk_config),
|
||||||
|
defer_selection_risk: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let rules = diagnostics
|
||||||
|
.risk_decisions
|
||||||
|
.iter()
|
||||||
|
.map(|decision| decision.rule_code.as_str())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
assert!(rules.contains("st"), "{:?}", diagnostics.risk_decisions);
|
||||||
|
assert!(rules.contains("kcb"), "{:?}", diagnostics.risk_decisions);
|
||||||
|
assert_eq!(
|
||||||
|
diagnostics.not_eligible_count,
|
||||||
|
diagnostics.risk_decisions.len()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
diagnostics.risk_decisions[0]
|
||||||
|
.diagnostic_line()
|
||||||
|
.starts_with("risk_decision=")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn selector_applies_configured_selection_risk_on_decision_date() {
|
||||||
|
let data = DataSet::from_components(
|
||||||
|
vec![
|
||||||
|
instrument("000001.SZ"),
|
||||||
|
instrument("688001.SH"),
|
||||||
|
instrument("000002.SZ"),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
market("000001.SZ", 10.0),
|
||||||
|
market("688001.SH", 10.0),
|
||||||
|
market("000002.SZ", 10.0),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
factor("000001.SZ", 8.0),
|
||||||
|
factor("688001.SH", 9.0),
|
||||||
|
factor("000002.SZ", 10.0),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
candidate("000001.SZ", true, false),
|
||||||
|
candidate("688001.SH", false, true),
|
||||||
|
candidate("000002.SZ", false, false),
|
||||||
|
],
|
||||||
|
vec![benchmark()],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let selector = DynamicMarketCapBandSelector::new(2000.0, 7.0, 10.0, 0.0, 10, 0.0, 0.0, 0.0);
|
||||||
|
let mut risk_config = FidcRiskControlConfig::default();
|
||||||
|
risk_config.static_rules.reject_st_selection = true;
|
||||||
|
risk_config.static_rules.reject_kcb_selection = true;
|
||||||
|
let (selected, diagnostics) = selector.select_with_diagnostics(&SelectionContext {
|
||||||
|
decision_date: d(),
|
||||||
|
benchmark: &benchmark(),
|
||||||
|
reference_level: 2000.0,
|
||||||
|
data: &data,
|
||||||
|
dynamic_universe: None,
|
||||||
|
risk_config: Some(&risk_config),
|
||||||
|
defer_selection_risk: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let selected_symbols = selected
|
||||||
|
.iter()
|
||||||
|
.map(|candidate| candidate.symbol.as_str())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
assert!(!selected_symbols.contains("000001.SZ"));
|
||||||
|
assert!(!selected_symbols.contains("688001.SH"));
|
||||||
|
assert!(selected_symbols.contains("000002.SZ"));
|
||||||
|
assert_eq!(diagnostics.not_eligible_count, 2);
|
||||||
|
let rules = diagnostics
|
||||||
|
.risk_decisions
|
||||||
|
.iter()
|
||||||
|
.map(|decision| decision.rule_code.as_str())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
assert!(rules.contains("st"), "{:?}", diagnostics.risk_decisions);
|
||||||
|
assert!(rules.contains("kcb"), "{:?}", diagnostics.risk_decisions);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn selector_can_defer_configured_selection_risk_without_losing_diagnostics() {
|
||||||
|
let data = DataSet::from_components(
|
||||||
|
vec![
|
||||||
|
instrument("000001.SZ"),
|
||||||
|
instrument("688001.SH"),
|
||||||
|
instrument("000002.SZ"),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
market("000001.SZ", 10.0),
|
||||||
|
market("688001.SH", 10.0),
|
||||||
|
market("000002.SZ", 10.0),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
factor("000001.SZ", 8.0),
|
||||||
|
factor("688001.SH", 9.0),
|
||||||
|
factor("000002.SZ", 10.0),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
candidate("000001.SZ", true, false),
|
||||||
|
candidate("688001.SH", false, true),
|
||||||
|
candidate("000002.SZ", false, false),
|
||||||
|
],
|
||||||
|
vec![benchmark()],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let selector = DynamicMarketCapBandSelector::new(2000.0, 7.0, 10.0, 0.0, 10, 0.0, 0.0, 0.0);
|
||||||
|
let mut risk_config = FidcRiskControlConfig::default();
|
||||||
|
risk_config.static_rules.reject_st_selection = true;
|
||||||
|
risk_config.static_rules.reject_kcb_selection = true;
|
||||||
|
|
||||||
|
let (selected, diagnostics) = selector.select_with_diagnostics(&SelectionContext {
|
||||||
|
decision_date: d(),
|
||||||
|
benchmark: &benchmark(),
|
||||||
|
reference_level: 2000.0,
|
||||||
|
data: &data,
|
||||||
|
dynamic_universe: None,
|
||||||
|
risk_config: Some(&risk_config),
|
||||||
|
defer_selection_risk: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let selected_symbols = selected
|
||||||
|
.iter()
|
||||||
|
.map(|candidate| candidate.symbol.as_str())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
assert!(selected_symbols.contains("000001.SZ"));
|
||||||
|
assert!(selected_symbols.contains("688001.SH"));
|
||||||
|
assert!(selected_symbols.contains("000002.SZ"));
|
||||||
|
let rules = diagnostics
|
||||||
|
.risk_decisions
|
||||||
|
.iter()
|
||||||
|
.map(|decision| decision.rule_code.as_str())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
assert!(rules.contains("st"), "{:?}", diagnostics.risk_decisions);
|
||||||
|
assert!(rules.contains("kcb"), "{:?}", diagnostics.risk_decisions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ fn candidate() -> CandidateEligibility {
|
|||||||
date: d(2024, 1, 3),
|
date: d(2024, 1, 3),
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -42,7 +43,7 @@ fn snapshot(open: f64, upper_limit: f64, lower_limit: f64) -> DailyMarketSnapsho
|
|||||||
ask1: open,
|
ask1: open,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 50_000,
|
bid1_volume: 50_000,
|
||||||
ask1_volume: 50_000,
|
ask1_volume: 50_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -66,6 +67,22 @@ fn china_cost_model_applies_minimum_commission_and_stamp_tax() {
|
|||||||
assert!((sell.stamp_tax - 100.0).abs() < 1e-9);
|
assert!((sell.stamp_tax - 100.0).abs() < 1e-9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aiquant_cost_model_matches_alv_run_options() {
|
||||||
|
let model = ChinaAShareCostModel::aiquant_default();
|
||||||
|
|
||||||
|
let buy = model.calculate(d(2026, 5, 19), OrderSide::Buy, 49_978.84);
|
||||||
|
assert!((buy.commission - 14.993652).abs() < 1e-9);
|
||||||
|
assert_eq!(buy.stamp_tax, 0.0);
|
||||||
|
|
||||||
|
let sell = model.calculate(d(2026, 5, 19), OrderSide::Sell, 100_724.72);
|
||||||
|
assert!((sell.commission - 30.217416).abs() < 1e-9);
|
||||||
|
assert!((sell.stamp_tax - 50.36236).abs() < 1e-9);
|
||||||
|
|
||||||
|
let small_buy = model.calculate(d(2026, 5, 19), OrderSide::Buy, 1_000.0);
|
||||||
|
assert!((small_buy.commission - 5.0).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn china_cost_model_switches_stamp_tax_rate_after_2023_08_28() {
|
fn china_cost_model_switches_stamp_tax_rate_after_2023_08_28() {
|
||||||
let model = ChinaAShareCostModel::default();
|
let model = ChinaAShareCostModel::default();
|
||||||
@@ -244,7 +261,7 @@ fn china_rule_hooks_allow_sell_when_last_price_is_above_lower_limit() {
|
|||||||
ask1: 2.53,
|
ask1: 2.53,
|
||||||
prev_close: 2.80,
|
prev_close: 2.80,
|
||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 50_000,
|
bid1_volume: 50_000,
|
||||||
ask1_volume: 50_000,
|
ask1_volume: 50_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
|
|||||||
@@ -88,10 +88,77 @@ impl Strategy for BuyAndHoldStrategy {
|
|||||||
},
|
},
|
||||||
notes: Vec::new(),
|
notes: Vec::new(),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
|
risk_decisions: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stock_market_snapshot(date: NaiveDate) -> DailyMarketSnapshot {
|
||||||
|
DailyMarketSnapshot {
|
||||||
|
date,
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
timestamp: Some(format!("{date} 10:18:00")),
|
||||||
|
day_open: 10.0,
|
||||||
|
open: 10.0,
|
||||||
|
high: 10.1,
|
||||||
|
low: 9.9,
|
||||||
|
close: 10.0,
|
||||||
|
last_price: 10.0,
|
||||||
|
bid1: 10.0,
|
||||||
|
ask1: 10.0,
|
||||||
|
prev_close: 10.0,
|
||||||
|
volume: 100_000,
|
||||||
|
minute_volume: 100_000,
|
||||||
|
bid1_volume: 100_000,
|
||||||
|
ask1_volume: 100_000,
|
||||||
|
trading_phase: Some("continuous".to_string()),
|
||||||
|
paused: false,
|
||||||
|
upper_limit: 11.0,
|
||||||
|
lower_limit: 9.0,
|
||||||
|
price_tick: 0.01,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stock_factor_snapshot(date: NaiveDate) -> DailyFactorSnapshot {
|
||||||
|
DailyFactorSnapshot {
|
||||||
|
date,
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
market_cap_bn: 20.0,
|
||||||
|
free_float_cap_bn: 18.0,
|
||||||
|
pe_ttm: 10.0,
|
||||||
|
turnover_ratio: Some(1.0),
|
||||||
|
effective_turnover_ratio: Some(1.0),
|
||||||
|
extra_factors: BTreeMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stock_candidate(date: NaiveDate) -> CandidateEligibility {
|
||||||
|
CandidateEligibility {
|
||||||
|
date,
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
|
is_new_listing: false,
|
||||||
|
is_paused: false,
|
||||||
|
allow_buy: true,
|
||||||
|
allow_sell: true,
|
||||||
|
is_kcb: false,
|
||||||
|
is_one_yuan: false,
|
||||||
|
risk_level_code: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn benchmark_snapshot(date: NaiveDate) -> BenchmarkSnapshot {
|
||||||
|
BenchmarkSnapshot {
|
||||||
|
date,
|
||||||
|
benchmark: "000300.SH".to_string(),
|
||||||
|
open: 100.0,
|
||||||
|
close: 100.0,
|
||||||
|
prev_close: 99.0,
|
||||||
|
volume: 1_000_000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn engine_reinvests_dividend_receivable_in_round_lots() {
|
fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||||
let buy_date = d(2025, 1, 1);
|
let buy_date = d(2025, 1, 1);
|
||||||
@@ -122,7 +189,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
|||||||
ask1: 10.0,
|
ask1: 10.0,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 100_000,
|
bid1_volume: 100_000,
|
||||||
ask1_volume: 100_000,
|
ask1_volume: 100_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -145,7 +212,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
|||||||
ask1: 10.0,
|
ask1: 10.0,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 100_000,
|
bid1_volume: 100_000,
|
||||||
ask1_volume: 100_000,
|
ask1_volume: 100_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -168,7 +235,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
|||||||
ask1: 10.0,
|
ask1: 10.0,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 100_000,
|
bid1_volume: 100_000,
|
||||||
ask1_volume: 100_000,
|
ask1_volume: 100_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -215,6 +282,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
|||||||
date: buy_date,
|
date: buy_date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -227,6 +295,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
|||||||
date: ex_date,
|
date: ex_date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -239,6 +308,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
|||||||
date: payable_date,
|
date: payable_date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -330,3 +400,96 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
|||||||
assert_eq!(reinvest_fill.commission, 0.0);
|
assert_eq!(reinvest_fill.commission, 0.0);
|
||||||
assert_eq!(reinvest_fill.stamp_tax, 0.0);
|
assert_eq!(reinvest_fill.stamp_tax, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn engine_settles_same_day_dividend_after_split_for_aiquant_semantics() {
|
||||||
|
let buy_date = d(2025, 1, 1);
|
||||||
|
let ex_date = d(2025, 1, 2);
|
||||||
|
let data = DataSet::from_components_with_actions(
|
||||||
|
vec![Instrument {
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
name: "Anchor".to_string(),
|
||||||
|
board: "SZ".to_string(),
|
||||||
|
round_lot: 100,
|
||||||
|
listed_at: Some(d(2020, 1, 1)),
|
||||||
|
delisted_at: None,
|
||||||
|
status: "active".to_string(),
|
||||||
|
}],
|
||||||
|
vec![
|
||||||
|
stock_market_snapshot(buy_date),
|
||||||
|
stock_market_snapshot(ex_date),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
stock_factor_snapshot(buy_date),
|
||||||
|
stock_factor_snapshot(ex_date),
|
||||||
|
],
|
||||||
|
vec![stock_candidate(buy_date), stock_candidate(ex_date)],
|
||||||
|
vec![benchmark_snapshot(buy_date), benchmark_snapshot(ex_date)],
|
||||||
|
vec![CorporateAction {
|
||||||
|
date: ex_date,
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
payable_date: Some(ex_date),
|
||||||
|
share_cash: 1.05,
|
||||||
|
share_bonus: 0.2,
|
||||||
|
share_gift: 0.0,
|
||||||
|
issue_quantity: 0.0,
|
||||||
|
issue_price: 0.0,
|
||||||
|
reform: false,
|
||||||
|
adjust_factor: None,
|
||||||
|
successor_symbol: None,
|
||||||
|
successor_ratio: None,
|
||||||
|
successor_cash: None,
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
.expect("dataset");
|
||||||
|
|
||||||
|
let mut engine = BacktestEngine::new(
|
||||||
|
data,
|
||||||
|
BuyAndHoldStrategy {
|
||||||
|
first_date: buy_date,
|
||||||
|
},
|
||||||
|
BrokerSimulator::new_with_execution_price(
|
||||||
|
ChinaAShareCostModel::default(),
|
||||||
|
ChinaEquityRuleHooks::default(),
|
||||||
|
PriceField::Open,
|
||||||
|
),
|
||||||
|
BacktestConfig {
|
||||||
|
initial_cash: 11_008.0,
|
||||||
|
benchmark_code: "000300.SH".to_string(),
|
||||||
|
start_date: Some(buy_date),
|
||||||
|
end_date: Some(ex_date),
|
||||||
|
decision_lag_trading_days: 0,
|
||||||
|
execution_price_field: PriceField::Open,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_dividend_reinvestment(true);
|
||||||
|
|
||||||
|
let result = engine.run().expect("backtest run");
|
||||||
|
let final_holding = result
|
||||||
|
.holdings_summary
|
||||||
|
.iter()
|
||||||
|
.find(|row| row.symbol == "000001.SZ")
|
||||||
|
.expect("holding");
|
||||||
|
assert_eq!(final_holding.quantity, 1_300);
|
||||||
|
|
||||||
|
let reinvest_fill = result
|
||||||
|
.fills
|
||||||
|
.iter()
|
||||||
|
.find(|fill| fill.reason == "dividend_reinvestment")
|
||||||
|
.expect("reinvestment fill");
|
||||||
|
assert_eq!(reinvest_fill.quantity, 100);
|
||||||
|
assert!((reinvest_fill.price - ((10.0 - 1.05) / 1.2)).abs() < 1e-9);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
result
|
||||||
|
.position_events
|
||||||
|
.iter()
|
||||||
|
.any(|event| event.reason == "stock_split 1.200000" && event.quantity_after == 1_200)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
result
|
||||||
|
.account_events
|
||||||
|
.iter()
|
||||||
|
.any(|event| event.note.contains("cash_receivable_reinvested"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
|||||||
ask1: 10.0,
|
ask1: 10.0,
|
||||||
prev_close: 9.8,
|
prev_close: 9.8,
|
||||||
volume: 10_000,
|
volume: 10_000,
|
||||||
tick_volume: 1_000,
|
minute_volume: 1_000,
|
||||||
bid1_volume: 10_000,
|
bid1_volume: 10_000,
|
||||||
ask1_volume: 10_000,
|
ask1_volume: 10_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -106,7 +106,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
|||||||
ask1: 10.6,
|
ask1: 10.6,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 10_000,
|
volume: 10_000,
|
||||||
tick_volume: 1_000,
|
minute_volume: 1_000,
|
||||||
bid1_volume: 10_000,
|
bid1_volume: 10_000,
|
||||||
ask1_volume: 10_000,
|
ask1_volume: 10_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -143,6 +143,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
|||||||
date: first,
|
date: first,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -155,6 +156,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
|||||||
date: second,
|
date: second,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -190,7 +192,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
|||||||
ChinaEquityRuleHooks,
|
ChinaEquityRuleHooks,
|
||||||
PriceField::Last,
|
PriceField::Last,
|
||||||
)
|
)
|
||||||
.with_matching_type(MatchingType::NextTickLast)
|
.with_matching_type(MatchingType::MinuteLast)
|
||||||
.with_intraday_execution_start_time(t(10, 40, 0));
|
.with_intraday_execution_start_time(t(10, 40, 0));
|
||||||
let config = BacktestConfig {
|
let config = BacktestConfig {
|
||||||
initial_cash: 10_000.0,
|
initial_cash: 10_000.0,
|
||||||
@@ -249,7 +251,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
|||||||
ask1: 10.0,
|
ask1: 10.0,
|
||||||
prev_close: 9.8,
|
prev_close: 9.8,
|
||||||
volume: 10_000,
|
volume: 10_000,
|
||||||
tick_volume: 1_000,
|
minute_volume: 1_000,
|
||||||
bid1_volume: 10_000,
|
bid1_volume: 10_000,
|
||||||
ask1_volume: 10_000,
|
ask1_volume: 10_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -272,7 +274,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
|||||||
ask1: 10.6,
|
ask1: 10.6,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 10_000,
|
volume: 10_000,
|
||||||
tick_volume: 1_000,
|
minute_volume: 1_000,
|
||||||
bid1_volume: 10_000,
|
bid1_volume: 10_000,
|
||||||
ask1_volume: 10_000,
|
ask1_volume: 10_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -309,6 +311,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
|||||||
date: first,
|
date: first,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -321,6 +324,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
|||||||
date: second,
|
date: second,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -385,7 +389,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
|||||||
ChinaEquityRuleHooks,
|
ChinaEquityRuleHooks,
|
||||||
PriceField::Last,
|
PriceField::Last,
|
||||||
)
|
)
|
||||||
.with_matching_type(MatchingType::NextTickLast)
|
.with_matching_type(MatchingType::MinuteLast)
|
||||||
.with_intraday_execution_start_time(t(10, 40, 0));
|
.with_intraday_execution_start_time(t(10, 40, 0));
|
||||||
let config = BacktestConfig {
|
let config = BacktestConfig {
|
||||||
initial_cash: 10_000.0,
|
initial_cash: 10_000.0,
|
||||||
@@ -480,7 +484,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
|||||||
ask1: 10.0,
|
ask1: 10.0,
|
||||||
prev_close: 9.8,
|
prev_close: 9.8,
|
||||||
volume: 10_000,
|
volume: 10_000,
|
||||||
tick_volume: 1_000,
|
minute_volume: 1_000,
|
||||||
bid1_volume: 10_000,
|
bid1_volume: 10_000,
|
||||||
ask1_volume: 10_000,
|
ask1_volume: 10_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -503,7 +507,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
|||||||
ask1: 10.6,
|
ask1: 10.6,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 10_000,
|
volume: 10_000,
|
||||||
tick_volume: 1_000,
|
minute_volume: 1_000,
|
||||||
bid1_volume: 10_000,
|
bid1_volume: 10_000,
|
||||||
ask1_volume: 10_000,
|
ask1_volume: 10_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -540,6 +544,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
|||||||
date: first,
|
date: first,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -552,6 +557,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
|||||||
date: second,
|
date: second,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -587,7 +593,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
|||||||
ChinaEquityRuleHooks,
|
ChinaEquityRuleHooks,
|
||||||
PriceField::Last,
|
PriceField::Last,
|
||||||
)
|
)
|
||||||
.with_matching_type(MatchingType::NextTickLast)
|
.with_matching_type(MatchingType::MinuteLast)
|
||||||
.with_intraday_execution_start_time(t(10, 40, 0));
|
.with_intraday_execution_start_time(t(10, 40, 0));
|
||||||
let config = BacktestConfig {
|
let config = BacktestConfig {
|
||||||
initial_cash: 10_000.0,
|
initial_cash: 10_000.0,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ impl Strategy for BuyThenHoldStrategy {
|
|||||||
}],
|
}],
|
||||||
notes: Vec::new(),
|
notes: Vec::new(),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
|
risk_decisions: Vec::new(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(StrategyDecision::default())
|
Ok(StrategyDecision::default())
|
||||||
@@ -81,7 +82,7 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
|||||||
ask1: 10.0,
|
ask1: 10.0,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 100_000,
|
bid1_volume: 100_000,
|
||||||
ask1_volume: 100_000,
|
ask1_volume: 100_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -104,7 +105,7 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
|||||||
ask1: 5.01,
|
ask1: 5.01,
|
||||||
prev_close: 5.0,
|
prev_close: 5.0,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 100_000,
|
bid1_volume: 100_000,
|
||||||
ask1_volume: 100_000,
|
ask1_volume: 100_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -113,6 +114,29 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
|||||||
lower_limit: 4.5,
|
lower_limit: 4.5,
|
||||||
price_tick: 0.01,
|
price_tick: 0.01,
|
||||||
},
|
},
|
||||||
|
DailyMarketSnapshot {
|
||||||
|
date: delist_date,
|
||||||
|
symbol: "000002.SZ".to_string(),
|
||||||
|
timestamp: Some("2025-01-03 10:18:00".to_string()),
|
||||||
|
day_open: 5.05,
|
||||||
|
open: 5.05,
|
||||||
|
high: 5.15,
|
||||||
|
low: 5.0,
|
||||||
|
close: 5.05,
|
||||||
|
last_price: 5.05,
|
||||||
|
bid1: 5.04,
|
||||||
|
ask1: 5.06,
|
||||||
|
prev_close: 5.0,
|
||||||
|
volume: 110_000,
|
||||||
|
minute_volume: 110_000,
|
||||||
|
bid1_volume: 110_000,
|
||||||
|
ask1_volume: 110_000,
|
||||||
|
trading_phase: Some("continuous".to_string()),
|
||||||
|
paused: false,
|
||||||
|
upper_limit: 5.5,
|
||||||
|
lower_limit: 4.5,
|
||||||
|
price_tick: 0.01,
|
||||||
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
@@ -127,7 +151,7 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
|||||||
ask1: 5.11,
|
ask1: 5.11,
|
||||||
prev_close: 5.0,
|
prev_close: 5.0,
|
||||||
volume: 120_000,
|
volume: 120_000,
|
||||||
tick_volume: 120_000,
|
minute_volume: 120_000,
|
||||||
bid1_volume: 120_000,
|
bid1_volume: 120_000,
|
||||||
ask1_volume: 120_000,
|
ask1_volume: 120_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -158,6 +182,16 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
|||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: BTreeMap::new(),
|
||||||
},
|
},
|
||||||
|
DailyFactorSnapshot {
|
||||||
|
date: delist_date,
|
||||||
|
symbol: "000002.SZ".to_string(),
|
||||||
|
market_cap_bn: 30.5,
|
||||||
|
free_float_cap_bn: 28.5,
|
||||||
|
pe_ttm: 10.0,
|
||||||
|
turnover_ratio: Some(1.0),
|
||||||
|
effective_turnover_ratio: Some(1.0),
|
||||||
|
extra_factors: BTreeMap::new(),
|
||||||
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
@@ -174,6 +208,7 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
|||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -186,6 +221,20 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
|||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
|
is_new_listing: false,
|
||||||
|
is_paused: false,
|
||||||
|
allow_buy: true,
|
||||||
|
allow_sell: true,
|
||||||
|
is_kcb: false,
|
||||||
|
is_one_yuan: false,
|
||||||
|
risk_level_code: None,
|
||||||
|
},
|
||||||
|
CandidateEligibility {
|
||||||
|
date: delist_date,
|
||||||
|
symbol: "000002.SZ".to_string(),
|
||||||
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -198,6 +247,7 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
|||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -216,6 +266,14 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
|||||||
prev_close: 99.0,
|
prev_close: 99.0,
|
||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
},
|
},
|
||||||
|
BenchmarkSnapshot {
|
||||||
|
date: delist_date,
|
||||||
|
benchmark: "000300.SH".to_string(),
|
||||||
|
open: 100.5,
|
||||||
|
close: 100.5,
|
||||||
|
prev_close: 100.0,
|
||||||
|
volume: 1_050_000,
|
||||||
|
},
|
||||||
BenchmarkSnapshot {
|
BenchmarkSnapshot {
|
||||||
date: date2,
|
date: date2,
|
||||||
benchmark: "000300.SH".to_string(),
|
benchmark: "000300.SH".to_string(),
|
||||||
@@ -304,7 +362,7 @@ fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
|
|||||||
ask1: 10.0,
|
ask1: 10.0,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 100_000,
|
bid1_volume: 100_000,
|
||||||
ask1_volume: 100_000,
|
ask1_volume: 100_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -327,7 +385,7 @@ fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
|
|||||||
ask1: 20.0,
|
ask1: 20.0,
|
||||||
prev_close: 20.0,
|
prev_close: 20.0,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 100_000,
|
bid1_volume: 100_000,
|
||||||
ask1_volume: 100_000,
|
ask1_volume: 100_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -350,7 +408,7 @@ fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
|
|||||||
ask1: 21.0,
|
ask1: 21.0,
|
||||||
prev_close: 20.0,
|
prev_close: 20.0,
|
||||||
volume: 120_000,
|
volume: 120_000,
|
||||||
tick_volume: 120_000,
|
minute_volume: 120_000,
|
||||||
bid1_volume: 120_000,
|
bid1_volume: 120_000,
|
||||||
ask1_volume: 120_000,
|
ask1_volume: 120_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -397,6 +455,7 @@ fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
|
|||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -409,6 +468,7 @@ fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
|
|||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -421,6 +481,7 @@ fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
|
|||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ fn single_day_anchor_data(date: NaiveDate) -> DataSet {
|
|||||||
ask1: 10.0,
|
ask1: 10.0,
|
||||||
prev_close: 9.9,
|
prev_close: 9.9,
|
||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
tick_volume: 1_000_000,
|
minute_volume: 1_000_000,
|
||||||
bid1_volume: 1_000_000,
|
bid1_volume: 1_000_000,
|
||||||
ask1_volume: 1_000_000,
|
ask1_volume: 1_000_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -81,6 +81,7 @@ fn single_day_anchor_data(date: NaiveDate) -> DataSet {
|
|||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -116,7 +117,7 @@ fn market_row(date: NaiveDate, symbol: &str, open: f64, close: f64) -> DailyMark
|
|||||||
ask1: close,
|
ask1: close,
|
||||||
prev_close: open,
|
prev_close: open,
|
||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
tick_volume: 1_000_000,
|
minute_volume: 1_000_000,
|
||||||
bid1_volume: 1_000_000,
|
bid1_volume: 1_000_000,
|
||||||
ask1_volume: 1_000_000,
|
ask1_volume: 1_000_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -149,6 +150,7 @@ fn candidate_row(date: NaiveDate, symbol: &str) -> CandidateEligibility {
|
|||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -294,6 +296,7 @@ impl Strategy for HookProbeStrategy {
|
|||||||
order_intents: Vec::new(),
|
order_intents: Vec::new(),
|
||||||
notes: Vec::new(),
|
notes: Vec::new(),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
|
risk_decisions: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,6 +339,7 @@ impl Strategy for AuctionOrderStrategy {
|
|||||||
}],
|
}],
|
||||||
notes: Vec::new(),
|
notes: Vec::new(),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
|
risk_decisions: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,6 +388,7 @@ impl Strategy for FuturesOrderStrategy {
|
|||||||
}],
|
}],
|
||||||
notes: Vec::new(),
|
notes: Vec::new(),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
|
risk_decisions: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -627,7 +632,7 @@ struct UniverseDirectiveStrategy {
|
|||||||
snapshots: Rc<RefCell<Vec<String>>>,
|
snapshots: Rc<RefCell<Vec<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TickProbeStrategy {
|
struct MinuteProbeStrategy {
|
||||||
seen_ticks: Rc<RefCell<Vec<String>>>,
|
seen_ticks: Rc<RefCell<Vec<String>>>,
|
||||||
ordered: bool,
|
ordered: bool,
|
||||||
}
|
}
|
||||||
@@ -716,6 +721,7 @@ impl Strategy for LimitCarryStrategy {
|
|||||||
}],
|
}],
|
||||||
notes: Vec::new(),
|
notes: Vec::new(),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
|
risk_decisions: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -793,13 +799,14 @@ impl Strategy for UniverseDirectiveStrategy {
|
|||||||
order_intents,
|
order_intents,
|
||||||
notes: Vec::new(),
|
notes: Vec::new(),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
|
risk_decisions: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Strategy for TickProbeStrategy {
|
impl Strategy for MinuteProbeStrategy {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
"tick-probe"
|
"minute-probe"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_day(
|
fn on_day(
|
||||||
@@ -812,26 +819,27 @@ impl Strategy for TickProbeStrategy {
|
|||||||
exit_symbols: BTreeSet::new(),
|
exit_symbols: BTreeSet::new(),
|
||||||
order_intents: vec![OrderIntent::Subscribe {
|
order_intents: vec![OrderIntent::Subscribe {
|
||||||
symbols: BTreeSet::from(["000001.SZ".to_string()]),
|
symbols: BTreeSet::from(["000001.SZ".to_string()]),
|
||||||
reason: "subscribe_tick_probe".to_string(),
|
reason: "subscribe_minute_probe".to_string(),
|
||||||
}],
|
}],
|
||||||
notes: Vec::new(),
|
notes: Vec::new(),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
|
risk_decisions: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_tick(
|
fn on_minute(
|
||||||
&mut self,
|
&mut self,
|
||||||
ctx: &StrategyContext<'_>,
|
ctx: &StrategyContext<'_>,
|
||||||
quote: &IntradayExecutionQuote,
|
quote: &IntradayExecutionQuote,
|
||||||
) -> Result<StrategyDecision, fidc_core::BacktestError> {
|
) -> Result<StrategyDecision, fidc_core::BacktestError> {
|
||||||
let visible_last = ctx
|
let visible_last = ctx
|
||||||
.history_bars("e.symbol, 9, "tick", "last", true)
|
.history_bars("e.symbol, 9, "1m", "last", true)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|value| format!("{value:.2}"))
|
.map(|value| format!("{value:.2}"))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(",");
|
.join(",");
|
||||||
let previous_last = ctx
|
let previous_last = ctx
|
||||||
.history_bars("e.symbol, 9, "tick", "last", false)
|
.history_bars("e.symbol, 9, "1m", "last", false)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|value| format!("{value:.2}"))
|
.map(|value| format!("{value:.2}"))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
@@ -853,10 +861,11 @@ impl Strategy for TickProbeStrategy {
|
|||||||
order_intents: vec![OrderIntent::Shares {
|
order_intents: vec![OrderIntent::Shares {
|
||||||
symbol: quote.symbol.clone(),
|
symbol: quote.symbol.clone(),
|
||||||
quantity: 100,
|
quantity: 100,
|
||||||
reason: "tick_buy".to_string(),
|
reason: "minute_buy".to_string(),
|
||||||
}],
|
}],
|
||||||
notes: Vec::new(),
|
notes: Vec::new(),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
|
risk_decisions: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -883,13 +892,13 @@ impl Strategy for DataApiProbeStrategy {
|
|||||||
.map(|value| format!("{value:.2}"))
|
.map(|value| format!("{value:.2}"))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(",");
|
.join(",");
|
||||||
let tick_last = ctx
|
let minute_last = ctx
|
||||||
.history_bars("000001.SZ", 2, "1m", "last", true)
|
.history_bars("000001.SZ", 2, "1m", "last", true)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|value| format!("{value:.2}"))
|
.map(|value| format!("{value:.2}"))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(",");
|
.join(",");
|
||||||
let previous_tick_last = ctx
|
let previous_minute_last = ctx
|
||||||
.history_bars("000001.SZ", 2, "1m", "last", false)
|
.history_bars("000001.SZ", 2, "1m", "last", false)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|value| format!("{value:.2}"))
|
.map(|value| format!("{value:.2}"))
|
||||||
@@ -919,14 +928,18 @@ impl Strategy for DataApiProbeStrategy {
|
|||||||
let daily_price_count = ctx
|
let daily_price_count = ctx
|
||||||
.get_price("000001.SZ", d(2025, 1, 3), ctx.execution_date, "1d")
|
.get_price("000001.SZ", d(2025, 1, 3), ctx.execution_date, "1d")
|
||||||
.len();
|
.len();
|
||||||
let tick_price_count = ctx
|
let minute_price_bars =
|
||||||
.get_price("000001.SZ", d(2025, 1, 3), ctx.execution_date, "tick")
|
ctx.get_price("000001.SZ", d(2025, 1, 3), ctx.execution_date, "1m");
|
||||||
.len();
|
let minute_price_count = minute_price_bars.len();
|
||||||
|
let minute_frequency = minute_price_bars
|
||||||
|
.first()
|
||||||
|
.map(|bar| bar.frequency.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
let instrument_history_count =
|
let instrument_history_count =
|
||||||
ctx.instruments_history(&["000001.SZ", "000002.SZ"]).len();
|
ctx.instruments_history(&["000001.SZ", "000002.SZ"]).len();
|
||||||
let active_instrument_count = ctx.active_instruments(&["000001.SZ", "000002.SZ"]).len();
|
let active_instrument_count = ctx.active_instruments(&["000001.SZ", "000002.SZ"]).len();
|
||||||
self.snapshots.borrow_mut().push(format!(
|
self.snapshots.borrow_mut().push(format!(
|
||||||
"daily={daily_close};previous={previous_close};tick={tick_last};previous_tick={previous_tick_last};current={current_close};instrument={instrument_name};all={};history={instrument_history_count};active={active_instrument_count};range={trading_date_count};prev={prev_date};next={next_date};suspended={suspended};st={st_flags};price_daily={daily_price_count};price_tick={tick_price_count}",
|
"daily={daily_close};previous={previous_close};minute={minute_last};previous_minute={previous_minute_last};current={current_close};instrument={instrument_name};all={};history={instrument_history_count};active={active_instrument_count};range={trading_date_count};prev={prev_date};next={next_date};suspended={suspended};st={st_flags};price_daily={daily_price_count};price_tick={minute_price_count};minute_frequency={minute_frequency}",
|
||||||
ctx.all_instruments().len()
|
ctx.all_instruments().len()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -954,6 +967,7 @@ impl Strategy for OrderInspectionStrategy {
|
|||||||
}],
|
}],
|
||||||
notes: Vec::new(),
|
notes: Vec::new(),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
|
risk_decisions: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1011,6 +1025,7 @@ impl Strategy for AccountFlowStrategy {
|
|||||||
],
|
],
|
||||||
notes: Vec::new(),
|
notes: Vec::new(),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
|
risk_decisions: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1052,7 +1067,7 @@ fn engine_runs_strategy_hooks_in_daily_order() {
|
|||||||
ask1: 10.0,
|
ask1: 10.0,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 100_000,
|
bid1_volume: 100_000,
|
||||||
ask1_volume: 100_000,
|
ask1_volume: 100_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -1075,7 +1090,7 @@ fn engine_runs_strategy_hooks_in_daily_order() {
|
|||||||
ask1: 10.1,
|
ask1: 10.1,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 110_000,
|
volume: 110_000,
|
||||||
tick_volume: 110_000,
|
minute_volume: 110_000,
|
||||||
bid1_volume: 110_000,
|
bid1_volume: 110_000,
|
||||||
ask1_volume: 110_000,
|
ask1_volume: 110_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -1112,6 +1127,7 @@ fn engine_runs_strategy_hooks_in_daily_order() {
|
|||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -1124,6 +1140,7 @@ fn engine_runs_strategy_hooks_in_daily_order() {
|
|||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -1248,7 +1265,7 @@ fn engine_executes_open_auction_decisions_before_on_day() {
|
|||||||
ask1: 10.0,
|
ask1: 10.0,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 100_000,
|
bid1_volume: 100_000,
|
||||||
ask1_volume: 100_000,
|
ask1_volume: 100_000,
|
||||||
trading_phase: Some("open_auction".to_string()),
|
trading_phase: Some("open_auction".to_string()),
|
||||||
@@ -1271,6 +1288,7 @@ fn engine_executes_open_auction_decisions_before_on_day() {
|
|||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -1347,7 +1365,7 @@ fn engine_executes_futures_order_intents_against_future_account() {
|
|||||||
ask1: 10.0,
|
ask1: 10.0,
|
||||||
prev_close: 9.9,
|
prev_close: 9.9,
|
||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
tick_volume: 1_000_000,
|
minute_volume: 1_000_000,
|
||||||
bid1_volume: 1_000_000,
|
bid1_volume: 1_000_000,
|
||||||
ask1_volume: 1_000_000,
|
ask1_volume: 1_000_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -1370,6 +1388,7 @@ fn engine_executes_futures_order_intents_against_future_account() {
|
|||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -1838,7 +1857,7 @@ fn engine_sweeps_futures_order_book_depth_when_available() {
|
|||||||
ChinaEquityRuleHooks::default(),
|
ChinaEquityRuleHooks::default(),
|
||||||
PriceField::Last,
|
PriceField::Last,
|
||||||
)
|
)
|
||||||
.with_matching_type(MatchingType::CounterpartyOffer);
|
.with_matching_type(MatchingType::MinuteBestCounterparty);
|
||||||
let mut engine = BacktestEngine::new(
|
let mut engine = BacktestEngine::new(
|
||||||
data,
|
data,
|
||||||
FuturesDepthLimitOrderStrategy,
|
FuturesDepthLimitOrderStrategy,
|
||||||
@@ -1911,7 +1930,7 @@ fn strategy_context_exposes_advanced_data_helpers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn engine_runs_subscribed_tick_hooks_and_executes_tick_orders() {
|
fn engine_runs_minute_hooks_and_executes_minute_orders() {
|
||||||
let date = d(2025, 1, 2);
|
let date = d(2025, 1, 2);
|
||||||
let data = DataSet::from_components_with_actions_and_quotes(
|
let data = DataSet::from_components_with_actions_and_quotes(
|
||||||
vec![Instrument {
|
vec![Instrument {
|
||||||
@@ -1937,7 +1956,7 @@ fn engine_runs_subscribed_tick_hooks_and_executes_tick_orders() {
|
|||||||
ask1: 10.2,
|
ask1: 10.2,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 100_000,
|
bid1_volume: 100_000,
|
||||||
ask1_volume: 100_000,
|
ask1_volume: 100_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -1960,6 +1979,7 @@ fn engine_runs_subscribed_tick_hooks_and_executes_tick_orders() {
|
|||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -2009,7 +2029,7 @@ fn engine_runs_subscribed_tick_hooks_and_executes_tick_orders() {
|
|||||||
.expect("dataset");
|
.expect("dataset");
|
||||||
|
|
||||||
let seen_ticks = Rc::new(RefCell::new(Vec::new()));
|
let seen_ticks = Rc::new(RefCell::new(Vec::new()));
|
||||||
let strategy = TickProbeStrategy {
|
let strategy = MinuteProbeStrategy {
|
||||||
seen_ticks: seen_ticks.clone(),
|
seen_ticks: seen_ticks.clone(),
|
||||||
ordered: false,
|
ordered: false,
|
||||||
};
|
};
|
||||||
@@ -2042,25 +2062,25 @@ fn engine_runs_subscribed_tick_hooks_and_executes_tick_orders() {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
assert_eq!(result.fills.len(), 1);
|
assert_eq!(result.fills.len(), 1);
|
||||||
assert_eq!(result.fills[0].reason, "tick_buy");
|
assert_eq!(result.fills[0].reason, "minute_buy");
|
||||||
assert_eq!(result.fills[0].quantity, 100);
|
assert_eq!(result.fills[0].quantity, 100);
|
||||||
assert!(
|
assert!(
|
||||||
result
|
result
|
||||||
.process_events
|
.process_events
|
||||||
.iter()
|
.iter()
|
||||||
.any(|event| event.kind == ProcessEventKind::PreTick)
|
.any(|event| event.kind == ProcessEventKind::PreMinute)
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
result
|
result
|
||||||
.process_events
|
.process_events
|
||||||
.iter()
|
.iter()
|
||||||
.any(|event| event.kind == ProcessEventKind::Tick)
|
.any(|event| event.kind == ProcessEventKind::Minute)
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
result
|
result
|
||||||
.process_events
|
.process_events
|
||||||
.iter()
|
.iter()
|
||||||
.any(|event| event.kind == ProcessEventKind::PostTick)
|
.any(|event| event.kind == ProcessEventKind::PostMinute)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2110,7 +2130,7 @@ fn strategy_context_exposes_engine_native_data_helpers() {
|
|||||||
ask1: close + 0.01,
|
ask1: close + 0.01,
|
||||||
prev_close,
|
prev_close,
|
||||||
volume,
|
volume,
|
||||||
tick_volume: volume,
|
minute_volume: volume,
|
||||||
bid1_volume: volume,
|
bid1_volume: volume,
|
||||||
ask1_volume: volume,
|
ask1_volume: volume,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -2144,6 +2164,7 @@ fn strategy_context_exposes_engine_native_data_helpers() {
|
|||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st,
|
is_st,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused,
|
is_paused,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -2249,7 +2270,7 @@ fn strategy_context_exposes_engine_native_data_helpers() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshots.borrow().as_slice(),
|
snapshots.borrow().as_slice(),
|
||||||
[
|
[
|
||||||
"daily=10.10,10.20;previous=10.00,10.10;tick=10.15,10.25;previous_tick=10.15;current=10.20;instrument=Anchor;all=2;history=2;active=1;range=3;prev=2025-01-03;next=2025-01-06;suspended=0,1,0;st=0,1,0;price_daily=2;price_tick=3"
|
"daily=10.10,10.20;previous=10.00,10.10;minute=10.15,10.25;previous_minute=10.15;current=10.20;instrument=Anchor;all=2;history=2;active=1;range=3;prev=2025-01-03;next=2025-01-06;suspended=0,1,0;st=0,1,0;price_daily=2;price_tick=3;minute_frequency=1m"
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2281,7 +2302,7 @@ fn strategy_context_exposes_final_order_runtime_view() {
|
|||||||
ask1: 10.2,
|
ask1: 10.2,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 100_000,
|
bid1_volume: 100_000,
|
||||||
ask1_volume: 100_000,
|
ask1_volume: 100_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -2304,6 +2325,7 @@ fn strategy_context_exposes_final_order_runtime_view() {
|
|||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -2498,7 +2520,7 @@ fn engine_applies_account_cash_flow_and_financing_intents() {
|
|||||||
ask1: 10.01,
|
ask1: 10.01,
|
||||||
prev_close: 9.9,
|
prev_close: 9.9,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 100_000,
|
bid1_volume: 100_000,
|
||||||
ask1_volume: 100_000,
|
ask1_volume: 100_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -2521,7 +2543,7 @@ fn engine_applies_account_cash_flow_and_financing_intents() {
|
|||||||
ask1: 10.01,
|
ask1: 10.01,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 100_000,
|
bid1_volume: 100_000,
|
||||||
ask1_volume: 100_000,
|
ask1_volume: 100_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -2558,6 +2580,7 @@ fn engine_applies_account_cash_flow_and_financing_intents() {
|
|||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -2570,6 +2593,7 @@ fn engine_applies_account_cash_flow_and_financing_intents() {
|
|||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -2682,7 +2706,7 @@ fn engine_rejects_pending_limit_orders_at_market_close() {
|
|||||||
ask1: 10.0,
|
ask1: 10.0,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 100_000,
|
bid1_volume: 100_000,
|
||||||
ask1_volume: 100_000,
|
ask1_volume: 100_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -2705,7 +2729,7 @@ fn engine_rejects_pending_limit_orders_at_market_close() {
|
|||||||
ask1: 9.7,
|
ask1: 9.7,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 100_000,
|
bid1_volume: 100_000,
|
||||||
ask1_volume: 100_000,
|
ask1_volume: 100_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -2742,6 +2766,7 @@ fn engine_rejects_pending_limit_orders_at_market_close() {
|
|||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -2754,6 +2779,7 @@ fn engine_rejects_pending_limit_orders_at_market_close() {
|
|||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -2852,7 +2878,7 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
|
|||||||
ask1: 10.0,
|
ask1: 10.0,
|
||||||
prev_close: 9.9,
|
prev_close: 9.9,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 100_000,
|
bid1_volume: 100_000,
|
||||||
ask1_volume: 100_000,
|
ask1_volume: 100_000,
|
||||||
trading_phase: Some("open_auction".to_string()),
|
trading_phase: Some("open_auction".to_string()),
|
||||||
@@ -2875,7 +2901,7 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
|
|||||||
ask1: 10.1,
|
ask1: 10.1,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 110_000,
|
volume: 110_000,
|
||||||
tick_volume: 110_000,
|
minute_volume: 110_000,
|
||||||
bid1_volume: 110_000,
|
bid1_volume: 110_000,
|
||||||
ask1_volume: 110_000,
|
ask1_volume: 110_000,
|
||||||
trading_phase: Some("open_auction".to_string()),
|
trading_phase: Some("open_auction".to_string()),
|
||||||
@@ -2898,7 +2924,7 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
|
|||||||
ask1: 10.2,
|
ask1: 10.2,
|
||||||
prev_close: 10.1,
|
prev_close: 10.1,
|
||||||
volume: 120_000,
|
volume: 120_000,
|
||||||
tick_volume: 120_000,
|
minute_volume: 120_000,
|
||||||
bid1_volume: 120_000,
|
bid1_volume: 120_000,
|
||||||
ask1_volume: 120_000,
|
ask1_volume: 120_000,
|
||||||
trading_phase: Some("open_auction".to_string()),
|
trading_phase: Some("open_auction".to_string()),
|
||||||
@@ -2945,6 +2971,7 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
|
|||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -2957,6 +2984,7 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
|
|||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -2969,6 +2997,7 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
|
|||||||
date: date3,
|
date: date3,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -3101,7 +3130,7 @@ fn engine_dispatches_process_events_to_external_bus_listeners() {
|
|||||||
ask1: 10.0,
|
ask1: 10.0,
|
||||||
prev_close: 9.9,
|
prev_close: 9.9,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 100_000,
|
bid1_volume: 100_000,
|
||||||
ask1_volume: 100_000,
|
ask1_volume: 100_000,
|
||||||
trading_phase: Some("open_auction".to_string()),
|
trading_phase: Some("open_auction".to_string()),
|
||||||
@@ -3124,7 +3153,7 @@ fn engine_dispatches_process_events_to_external_bus_listeners() {
|
|||||||
ask1: 10.1,
|
ask1: 10.1,
|
||||||
prev_close: 10.0,
|
prev_close: 10.0,
|
||||||
volume: 110_000,
|
volume: 110_000,
|
||||||
tick_volume: 110_000,
|
minute_volume: 110_000,
|
||||||
bid1_volume: 110_000,
|
bid1_volume: 110_000,
|
||||||
ask1_volume: 110_000,
|
ask1_volume: 110_000,
|
||||||
trading_phase: Some("open_auction".to_string()),
|
trading_phase: Some("open_auction".to_string()),
|
||||||
@@ -3147,7 +3176,7 @@ fn engine_dispatches_process_events_to_external_bus_listeners() {
|
|||||||
ask1: 10.2,
|
ask1: 10.2,
|
||||||
prev_close: 10.1,
|
prev_close: 10.1,
|
||||||
volume: 120_000,
|
volume: 120_000,
|
||||||
tick_volume: 120_000,
|
minute_volume: 120_000,
|
||||||
bid1_volume: 120_000,
|
bid1_volume: 120_000,
|
||||||
ask1_volume: 120_000,
|
ask1_volume: 120_000,
|
||||||
trading_phase: Some("open_auction".to_string()),
|
trading_phase: Some("open_auction".to_string()),
|
||||||
@@ -3194,6 +3223,7 @@ fn engine_dispatches_process_events_to_external_bus_listeners() {
|
|||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -3206,6 +3236,7 @@ fn engine_dispatches_process_events_to_external_bus_listeners() {
|
|||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -3218,6 +3249,7 @@ fn engine_dispatches_process_events_to_external_bus_listeners() {
|
|||||||
date: date3,
|
date: date3,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -3481,7 +3513,7 @@ fn engine_applies_dynamic_universe_and_subscription_directives() {
|
|||||||
ask1: 10.01,
|
ask1: 10.01,
|
||||||
prev_close: 9.95,
|
prev_close: 9.95,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 5_000,
|
minute_volume: 5_000,
|
||||||
bid1_volume: 2_000,
|
bid1_volume: 2_000,
|
||||||
ask1_volume: 2_000,
|
ask1_volume: 2_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -3504,7 +3536,7 @@ fn engine_applies_dynamic_universe_and_subscription_directives() {
|
|||||||
ask1: 20.01,
|
ask1: 20.01,
|
||||||
prev_close: 19.95,
|
prev_close: 19.95,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 5_000,
|
minute_volume: 5_000,
|
||||||
bid1_volume: 2_000,
|
bid1_volume: 2_000,
|
||||||
ask1_volume: 2_000,
|
ask1_volume: 2_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -3551,6 +3583,7 @@ fn engine_applies_dynamic_universe_and_subscription_directives() {
|
|||||||
date: *date,
|
date: *date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -3563,6 +3596,7 @@ fn engine_applies_dynamic_universe_and_subscription_directives() {
|
|||||||
date: *date,
|
date: *date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -3663,7 +3697,7 @@ fn engine_exposes_current_process_context_to_strategies() {
|
|||||||
ask1: 10.0,
|
ask1: 10.0,
|
||||||
prev_close: 9.9,
|
prev_close: 9.9,
|
||||||
volume: 100_000,
|
volume: 100_000,
|
||||||
tick_volume: 100_000,
|
minute_volume: 100_000,
|
||||||
bid1_volume: 100_000,
|
bid1_volume: 100_000,
|
||||||
ask1_volume: 100_000,
|
ask1_volume: 100_000,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
@@ -3686,6 +3720,7 @@ fn engine_exposes_current_process_context_to_strategies() {
|
|||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
allow_buy: true,
|
allow_buy: true,
|
||||||
@@ -3761,6 +3796,7 @@ impl Strategy for BuyMissingRowThenHoldStrategy {
|
|||||||
}],
|
}],
|
||||||
notes: Vec::new(),
|
notes: Vec::new(),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
|
risk_decisions: Vec::new(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(StrategyDecision::default())
|
Ok(StrategyDecision::default())
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,93 +0,0 @@
|
|||||||
use fidc_core::DataSet;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
fn temp_dir() -> PathBuf {
|
|
||||||
let uniq = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.expect("clock")
|
|
||||||
.as_nanos();
|
|
||||||
let dir = std::env::temp_dir().join(format!("fidc-bt-partitioned-{uniq}"));
|
|
||||||
fs::create_dir_all(&dir).expect("mkdir temp");
|
|
||||||
dir
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn can_load_partitioned_snapshot_dir() {
|
|
||||||
let dir = temp_dir();
|
|
||||||
fs::create_dir_all(dir.join("benchmark/2024/01")).unwrap();
|
|
||||||
fs::create_dir_all(dir.join("market/2024/01")).unwrap();
|
|
||||||
fs::create_dir_all(dir.join("factors/2024/01")).unwrap();
|
|
||||||
fs::create_dir_all(dir.join("candidates/2024/01")).unwrap();
|
|
||||||
fs::create_dir_all(dir.join("corporate_actions/2024/01")).unwrap();
|
|
||||||
|
|
||||||
fs::write(
|
|
||||||
dir.join("instruments.csv"),
|
|
||||||
"symbol,name,board,round_lot,listed_at,delisted_at,status\n000001.SZ,PingAn,SZ,100,2020-01-01,,active\n",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
fs::write(
|
|
||||||
dir.join("benchmark/2024/01/2024-01-02.csv"),
|
|
||||||
"date,benchmark,open,close,prev_close,volume\n2024-01-02,CSI300.DEMO,2990,3000,2980,100000000\n",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
fs::write(
|
|
||||||
dir.join("market/2024/01/2024-01-02.csv"),
|
|
||||||
"date,symbol,open,high,low,close,prev_close,volume,paused,upper_limit,lower_limit,day_open,last_price,bid1,ask1,price_tick\n2024-01-02,000001.SZ,10,10.5,9.9,10.2,10,100000,false,11,9,10.1,10.15,10.14,10.16,0.01\n",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
fs::write(
|
|
||||||
dir.join("factors/2024/01/2024-01-02.csv"),
|
|
||||||
"date,symbol,market_cap_bn,free_float_cap_bn,pe_ttm,turnover_ratio,effective_turnover_ratio\n2024-01-02,000001.SZ,40,35,12,3.2,2.1\n",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
fs::write(
|
|
||||||
dir.join("candidates/2024/01/2024-01-02.csv"),
|
|
||||||
"date,symbol,is_st,is_new_listing,is_paused,allow_buy,allow_sell,is_kcb,is_one_yuan\n2024-01-02,000001.SZ,false,false,false,true,true,false,false\n",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
fs::write(
|
|
||||||
dir.join("corporate_actions/2024/01/2024-01-02.csv"),
|
|
||||||
"date,symbol,payable_date,share_cash,share_bonus,share_gift,issue_quantity,issue_price,reform,adjust_factor\n2024-01-02,000001.SZ,2024-01-05,0.5,0.1,0.0,0,0,false,1.05\n",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let data = DataSet::from_partitioned_dir(&dir).expect("partitioned dataset");
|
|
||||||
assert_eq!(data.benchmark_code(), "CSI300.DEMO");
|
|
||||||
assert!(
|
|
||||||
data.market_snapshots_on(chrono::NaiveDate::from_ymd_opt(2024, 1, 2).unwrap())
|
|
||||||
.len()
|
|
||||||
== 1
|
|
||||||
);
|
|
||||||
let market_rows =
|
|
||||||
data.market_snapshots_on(chrono::NaiveDate::from_ymd_opt(2024, 1, 2).unwrap());
|
|
||||||
let snapshot = market_rows.first().expect("market snapshot");
|
|
||||||
assert_eq!(snapshot.day_open, 10.1);
|
|
||||||
assert_eq!(snapshot.last_price, 10.15);
|
|
||||||
assert_eq!(snapshot.price_tick, 0.01);
|
|
||||||
assert_eq!(
|
|
||||||
data.instruments()
|
|
||||||
.get("000001.SZ")
|
|
||||||
.expect("instrument")
|
|
||||||
.round_lot,
|
|
||||||
100
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
data.instruments()
|
|
||||||
.get("000001.SZ")
|
|
||||||
.expect("instrument")
|
|
||||||
.listed_at,
|
|
||||||
Some(chrono::NaiveDate::from_ymd_opt(2020, 1, 1).unwrap())
|
|
||||||
);
|
|
||||||
let actions = data.corporate_actions_on(chrono::NaiveDate::from_ymd_opt(2024, 1, 2).unwrap());
|
|
||||||
assert_eq!(actions.len(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
actions[0].payable_date,
|
|
||||||
Some(chrono::NaiveDate::from_ymd_opt(2024, 1, 5).unwrap())
|
|
||||||
);
|
|
||||||
assert!((actions[0].share_cash - 0.5).abs() < 1e-9);
|
|
||||||
assert!((actions[0].split_ratio() - 1.1).abs() < 1e-9);
|
|
||||||
|
|
||||||
let _ = fs::remove_dir_all(&dir);
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,582 @@
|
|||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use fidc_core::{
|
use fidc_core::{
|
||||||
CnSmallCapRotationConfig, CnSmallCapRotationStrategy, DataSet, OmniMicroCapConfig,
|
BenchmarkSnapshot, CandidateEligibility, CnSmallCapRotationConfig, CnSmallCapRotationStrategy,
|
||||||
|
DailyFactorSnapshot, DailyMarketSnapshot, DataSet, Instrument, OmniMicroCapConfig,
|
||||||
OmniMicroCapStrategy, PortfolioState, Strategy, StrategyContext,
|
OmniMicroCapStrategy, PortfolioState, Strategy, StrategyContext,
|
||||||
};
|
};
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::path::PathBuf;
|
|
||||||
|
fn d(value: &str) -> NaiveDate {
|
||||||
|
NaiveDate::parse_from_str(value, "%Y-%m-%d").unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instrument(symbol: &str, name: &str) -> Instrument {
|
||||||
|
Instrument {
|
||||||
|
symbol: symbol.to_string(),
|
||||||
|
name: name.to_string(),
|
||||||
|
board: "Main".to_string(),
|
||||||
|
round_lot: 100,
|
||||||
|
listed_at: None,
|
||||||
|
delisted_at: None,
|
||||||
|
status: "active".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn market(
|
||||||
|
date: &str,
|
||||||
|
symbol: &str,
|
||||||
|
open: f64,
|
||||||
|
high: f64,
|
||||||
|
low: f64,
|
||||||
|
close: f64,
|
||||||
|
prev_close: f64,
|
||||||
|
volume: u64,
|
||||||
|
paused: bool,
|
||||||
|
) -> DailyMarketSnapshot {
|
||||||
|
DailyMarketSnapshot {
|
||||||
|
date: d(date),
|
||||||
|
symbol: symbol.to_string(),
|
||||||
|
timestamp: None,
|
||||||
|
day_open: open,
|
||||||
|
open,
|
||||||
|
high,
|
||||||
|
low,
|
||||||
|
close,
|
||||||
|
last_price: close,
|
||||||
|
bid1: close,
|
||||||
|
ask1: close,
|
||||||
|
prev_close,
|
||||||
|
volume,
|
||||||
|
minute_volume: 0,
|
||||||
|
bid1_volume: 0,
|
||||||
|
ask1_volume: 0,
|
||||||
|
trading_phase: None,
|
||||||
|
paused,
|
||||||
|
upper_limit: (prev_close * 1.10 * 100.0).round() / 100.0,
|
||||||
|
lower_limit: (prev_close * 0.90 * 100.0).round() / 100.0,
|
||||||
|
price_tick: 0.01,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn factor(
|
||||||
|
date: &str,
|
||||||
|
symbol: &str,
|
||||||
|
market_cap_bn: f64,
|
||||||
|
free_float_cap_bn: f64,
|
||||||
|
) -> DailyFactorSnapshot {
|
||||||
|
DailyFactorSnapshot {
|
||||||
|
date: d(date),
|
||||||
|
symbol: symbol.to_string(),
|
||||||
|
market_cap_bn,
|
||||||
|
free_float_cap_bn,
|
||||||
|
pe_ttm: 18.0,
|
||||||
|
turnover_ratio: None,
|
||||||
|
effective_turnover_ratio: None,
|
||||||
|
extra_factors: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn candidate(
|
||||||
|
date: &str,
|
||||||
|
symbol: &str,
|
||||||
|
is_new_listing: bool,
|
||||||
|
is_paused: bool,
|
||||||
|
allow_buy: bool,
|
||||||
|
allow_sell: bool,
|
||||||
|
) -> CandidateEligibility {
|
||||||
|
CandidateEligibility {
|
||||||
|
date: d(date),
|
||||||
|
symbol: symbol.to_string(),
|
||||||
|
is_st: false,
|
||||||
|
is_star_st: false,
|
||||||
|
is_new_listing,
|
||||||
|
is_paused,
|
||||||
|
allow_buy,
|
||||||
|
allow_sell,
|
||||||
|
is_kcb: false,
|
||||||
|
is_one_yuan: false,
|
||||||
|
risk_level_code: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn benchmark(date: &str, open: f64, close: f64, prev_close: f64, volume: u64) -> BenchmarkSnapshot {
|
||||||
|
BenchmarkSnapshot {
|
||||||
|
date: d(date),
|
||||||
|
benchmark: "CSI300.DEMO".to_string(),
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
prev_close,
|
||||||
|
volume,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strategy_test_dataset() -> DataSet {
|
||||||
|
let dates = [
|
||||||
|
"2024-01-02",
|
||||||
|
"2024-01-03",
|
||||||
|
"2024-01-04",
|
||||||
|
"2024-01-05",
|
||||||
|
"2024-01-08",
|
||||||
|
"2024-01-09",
|
||||||
|
"2024-01-10",
|
||||||
|
"2024-01-11",
|
||||||
|
"2024-01-12",
|
||||||
|
];
|
||||||
|
let instruments = vec![
|
||||||
|
instrument("000001.SZ", "Alpha Components"),
|
||||||
|
instrument("000002.SZ", "Beta Precision"),
|
||||||
|
instrument("000003.SZ", "Charlie Materials"),
|
||||||
|
instrument("600001.SH", "Delta Industrials"),
|
||||||
|
];
|
||||||
|
let market = vec![
|
||||||
|
market(
|
||||||
|
"2024-01-02",
|
||||||
|
"000001.SZ",
|
||||||
|
10.0,
|
||||||
|
10.2,
|
||||||
|
9.9,
|
||||||
|
10.1,
|
||||||
|
9.8,
|
||||||
|
1_200_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-02",
|
||||||
|
"000002.SZ",
|
||||||
|
11.0,
|
||||||
|
11.3,
|
||||||
|
10.9,
|
||||||
|
11.2,
|
||||||
|
10.8,
|
||||||
|
1_100_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-02",
|
||||||
|
"000003.SZ",
|
||||||
|
8.0,
|
||||||
|
8.1,
|
||||||
|
7.8,
|
||||||
|
7.9,
|
||||||
|
8.0,
|
||||||
|
900_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-02",
|
||||||
|
"600001.SH",
|
||||||
|
15.0,
|
||||||
|
15.2,
|
||||||
|
14.9,
|
||||||
|
15.1,
|
||||||
|
15.0,
|
||||||
|
800_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-03",
|
||||||
|
"000001.SZ",
|
||||||
|
10.2,
|
||||||
|
10.5,
|
||||||
|
10.1,
|
||||||
|
10.4,
|
||||||
|
10.1,
|
||||||
|
1_250_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-03",
|
||||||
|
"000002.SZ",
|
||||||
|
11.2,
|
||||||
|
11.6,
|
||||||
|
11.1,
|
||||||
|
11.5,
|
||||||
|
11.2,
|
||||||
|
1_120_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-03",
|
||||||
|
"000003.SZ",
|
||||||
|
7.8,
|
||||||
|
7.9,
|
||||||
|
7.3,
|
||||||
|
7.4,
|
||||||
|
7.9,
|
||||||
|
930_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-03",
|
||||||
|
"600001.SH",
|
||||||
|
15.1,
|
||||||
|
15.3,
|
||||||
|
15.0,
|
||||||
|
15.2,
|
||||||
|
15.1,
|
||||||
|
820_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-04",
|
||||||
|
"000001.SZ",
|
||||||
|
10.5,
|
||||||
|
10.8,
|
||||||
|
10.4,
|
||||||
|
10.7,
|
||||||
|
10.4,
|
||||||
|
1_280_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-04",
|
||||||
|
"000002.SZ",
|
||||||
|
11.4,
|
||||||
|
11.9,
|
||||||
|
11.3,
|
||||||
|
11.8,
|
||||||
|
11.5,
|
||||||
|
1_150_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-04",
|
||||||
|
"000003.SZ",
|
||||||
|
7.3,
|
||||||
|
7.4,
|
||||||
|
7.0,
|
||||||
|
7.1,
|
||||||
|
7.4,
|
||||||
|
940_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-04",
|
||||||
|
"600001.SH",
|
||||||
|
15.2,
|
||||||
|
15.5,
|
||||||
|
15.1,
|
||||||
|
15.4,
|
||||||
|
15.2,
|
||||||
|
830_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-05",
|
||||||
|
"000001.SZ",
|
||||||
|
10.8,
|
||||||
|
11.1,
|
||||||
|
10.7,
|
||||||
|
11.0,
|
||||||
|
10.7,
|
||||||
|
1_300_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-05",
|
||||||
|
"000002.SZ",
|
||||||
|
11.9,
|
||||||
|
12.1,
|
||||||
|
11.8,
|
||||||
|
12.0,
|
||||||
|
11.8,
|
||||||
|
1_180_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-05",
|
||||||
|
"000003.SZ",
|
||||||
|
7.0,
|
||||||
|
7.1,
|
||||||
|
6.8,
|
||||||
|
6.9,
|
||||||
|
7.1,
|
||||||
|
950_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-05",
|
||||||
|
"600001.SH",
|
||||||
|
15.4,
|
||||||
|
15.6,
|
||||||
|
15.3,
|
||||||
|
15.5,
|
||||||
|
15.4,
|
||||||
|
840_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-08",
|
||||||
|
"000001.SZ",
|
||||||
|
11.1,
|
||||||
|
11.6,
|
||||||
|
11.0,
|
||||||
|
11.5,
|
||||||
|
11.0,
|
||||||
|
1_400_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-08",
|
||||||
|
"000002.SZ",
|
||||||
|
12.1,
|
||||||
|
12.5,
|
||||||
|
12.0,
|
||||||
|
12.4,
|
||||||
|
12.0,
|
||||||
|
1_200_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-08",
|
||||||
|
"000003.SZ",
|
||||||
|
7.0,
|
||||||
|
7.3,
|
||||||
|
6.9,
|
||||||
|
7.2,
|
||||||
|
6.9,
|
||||||
|
980_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-08",
|
||||||
|
"600001.SH",
|
||||||
|
15.5,
|
||||||
|
15.7,
|
||||||
|
15.4,
|
||||||
|
15.6,
|
||||||
|
15.5,
|
||||||
|
850_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-09",
|
||||||
|
"000001.SZ",
|
||||||
|
11.6,
|
||||||
|
12.4,
|
||||||
|
11.5,
|
||||||
|
12.3,
|
||||||
|
11.5,
|
||||||
|
1_500_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-09",
|
||||||
|
"000002.SZ",
|
||||||
|
12.5,
|
||||||
|
12.9,
|
||||||
|
12.4,
|
||||||
|
12.8,
|
||||||
|
12.4,
|
||||||
|
1_250_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-09",
|
||||||
|
"000003.SZ",
|
||||||
|
7.2,
|
||||||
|
7.5,
|
||||||
|
7.1,
|
||||||
|
7.4,
|
||||||
|
7.2,
|
||||||
|
990_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-09",
|
||||||
|
"600001.SH",
|
||||||
|
15.6,
|
||||||
|
15.7,
|
||||||
|
15.4,
|
||||||
|
15.5,
|
||||||
|
15.6,
|
||||||
|
860_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-10",
|
||||||
|
"000001.SZ",
|
||||||
|
12.2,
|
||||||
|
12.3,
|
||||||
|
11.9,
|
||||||
|
12.0,
|
||||||
|
12.3,
|
||||||
|
1_450_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-10",
|
||||||
|
"000002.SZ",
|
||||||
|
12.7,
|
||||||
|
12.8,
|
||||||
|
12.5,
|
||||||
|
12.6,
|
||||||
|
12.8,
|
||||||
|
1_220_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-10",
|
||||||
|
"000003.SZ",
|
||||||
|
7.5,
|
||||||
|
7.6,
|
||||||
|
7.4,
|
||||||
|
7.5,
|
||||||
|
7.4,
|
||||||
|
1_000_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-10",
|
||||||
|
"600001.SH",
|
||||||
|
15.4,
|
||||||
|
15.5,
|
||||||
|
15.1,
|
||||||
|
15.2,
|
||||||
|
15.5,
|
||||||
|
870_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-11",
|
||||||
|
"000001.SZ",
|
||||||
|
12.0,
|
||||||
|
12.1,
|
||||||
|
11.5,
|
||||||
|
11.6,
|
||||||
|
12.0,
|
||||||
|
1_420_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-11",
|
||||||
|
"000002.SZ",
|
||||||
|
12.5,
|
||||||
|
12.6,
|
||||||
|
12.1,
|
||||||
|
12.2,
|
||||||
|
12.6,
|
||||||
|
1_210_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-11",
|
||||||
|
"000003.SZ",
|
||||||
|
7.4,
|
||||||
|
7.5,
|
||||||
|
7.2,
|
||||||
|
7.3,
|
||||||
|
7.5,
|
||||||
|
980_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-11",
|
||||||
|
"600001.SH",
|
||||||
|
15.2,
|
||||||
|
15.2,
|
||||||
|
15.2,
|
||||||
|
15.2,
|
||||||
|
15.2,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-12",
|
||||||
|
"000001.SZ",
|
||||||
|
11.5,
|
||||||
|
11.6,
|
||||||
|
11.1,
|
||||||
|
11.2,
|
||||||
|
11.6,
|
||||||
|
1_380_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-12",
|
||||||
|
"000002.SZ",
|
||||||
|
12.1,
|
||||||
|
12.2,
|
||||||
|
11.8,
|
||||||
|
11.9,
|
||||||
|
12.2,
|
||||||
|
1_190_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-12",
|
||||||
|
"000003.SZ",
|
||||||
|
7.2,
|
||||||
|
7.2,
|
||||||
|
6.9,
|
||||||
|
7.0,
|
||||||
|
7.3,
|
||||||
|
960_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
market(
|
||||||
|
"2024-01-12",
|
||||||
|
"600001.SH",
|
||||||
|
14.8,
|
||||||
|
15.0,
|
||||||
|
14.7,
|
||||||
|
14.9,
|
||||||
|
15.2,
|
||||||
|
850_000,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let factors = dates
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.flat_map(|(idx, date)| {
|
||||||
|
let i = idx as f64;
|
||||||
|
[
|
||||||
|
factor(date, "000001.SZ", 38.0 + i, 24.0 + i * 0.5),
|
||||||
|
factor(date, "000002.SZ", 45.0 + i, 30.0 + i * 0.5),
|
||||||
|
factor(date, "000003.SZ", 65.0 - i, 40.0 - i * 0.5),
|
||||||
|
factor(date, "600001.SH", 85.0 + i, 55.0 + i * 0.5),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let candidates = dates
|
||||||
|
.iter()
|
||||||
|
.flat_map(|date| {
|
||||||
|
let first_two = *date == "2024-01-02" || *date == "2024-01-03";
|
||||||
|
let paused_600001 = *date == "2024-01-11";
|
||||||
|
[
|
||||||
|
candidate(date, "000001.SZ", first_two, false, !first_two, true),
|
||||||
|
candidate(date, "000002.SZ", false, false, true, true),
|
||||||
|
candidate(date, "000003.SZ", false, false, true, true),
|
||||||
|
candidate(
|
||||||
|
date,
|
||||||
|
"600001.SH",
|
||||||
|
false,
|
||||||
|
paused_600001,
|
||||||
|
!paused_600001,
|
||||||
|
!paused_600001,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let benchmarks = vec![
|
||||||
|
benchmark("2024-01-02", 2990.0, 3000.0, 2980.0, 100_000_000),
|
||||||
|
benchmark("2024-01-03", 3005.0, 3020.0, 3000.0, 102_000_000),
|
||||||
|
benchmark("2024-01-04", 3025.0, 3050.0, 3020.0, 105_000_000),
|
||||||
|
benchmark("2024-01-05", 3055.0, 3080.0, 3050.0, 108_000_000),
|
||||||
|
benchmark("2024-01-08", 3085.0, 3110.0, 3080.0, 109_000_000),
|
||||||
|
benchmark("2024-01-09", 3100.0, 3090.0, 3110.0, 107_000_000),
|
||||||
|
benchmark("2024-01-10", 3080.0, 3040.0, 3090.0, 111_000_000),
|
||||||
|
benchmark("2024-01-11", 3030.0, 2990.0, 3040.0, 115_000_000),
|
||||||
|
benchmark("2024-01-12", 2980.0, 2950.0, 2990.0, 118_000_000),
|
||||||
|
];
|
||||||
|
DataSet::from_components(instruments, market, factors, candidates, benchmarks)
|
||||||
|
.expect("strategy test dataset")
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn strategy_emits_target_weights_and_diagnostics() {
|
fn strategy_emits_target_weights_and_diagnostics() {
|
||||||
let data_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../data/demo");
|
let data = strategy_test_dataset();
|
||||||
let data = DataSet::from_csv_dir(&data_dir).expect("demo data");
|
|
||||||
let decision_date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
|
let decision_date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
|
||||||
let execution_date = NaiveDate::from_ymd_opt(2024, 1, 11).unwrap();
|
let execution_date = NaiveDate::from_ymd_opt(2024, 1, 11).unwrap();
|
||||||
let portfolio = PortfolioState::new(1_000_000.0);
|
let portfolio = PortfolioState::new(1_000_000.0);
|
||||||
@@ -53,8 +620,7 @@ fn strategy_emits_target_weights_and_diagnostics() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn omni_strategy_emits_same_day_decision() {
|
fn omni_strategy_emits_same_day_decision() {
|
||||||
let data_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../data/demo");
|
let data = strategy_test_dataset();
|
||||||
let data = DataSet::from_csv_dir(&data_dir).expect("demo data");
|
|
||||||
let execution_date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
|
let execution_date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
|
||||||
let portfolio = PortfolioState::new(1_000_000.0);
|
let portfolio = PortfolioState::new(1_000_000.0);
|
||||||
let mut cfg = OmniMicroCapConfig::omni_microcap();
|
let mut cfg = OmniMicroCapConfig::omni_microcap();
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
let refresh_rate = 15;
|
|
||||||
let stocknum = 40;
|
|
||||||
let close_rate = 1.07;
|
|
||||||
let loss_rate = 0.93;
|
|
||||||
let rsi_rate = 1.0001;
|
|
||||||
let trade_rate = 0.5;
|
|
||||||
let xs = 4 / 500;
|
|
||||||
let base_index_level = 2000;
|
|
||||||
let base_cap_floor = 3;
|
|
||||||
let base_cap_ceiling = 28;
|
|
||||||
|
|
||||||
fn band_start(current_price, base_index_level, xs, base_cap_floor) {
|
|
||||||
if current_price == base_index_level {
|
|
||||||
base_cap_floor
|
|
||||||
} else if current_price > 0 {
|
|
||||||
round((current_price - base_index_level) * xs + base_cap_floor)
|
|
||||||
} else {
|
|
||||||
base_cap_floor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn band_end(current_price, base_index_level, xs, base_cap_ceiling) {
|
|
||||||
if current_price == base_index_level {
|
|
||||||
base_cap_ceiling
|
|
||||||
} else if current_price > 0 {
|
|
||||||
round((current_price - base_index_level) * xs + base_cap_ceiling)
|
|
||||||
} else {
|
|
||||||
base_cap_ceiling
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
strategy("microcap_volume_trend_000852") {
|
|
||||||
market("CN_A")
|
|
||||||
benchmark("000852.SH")
|
|
||||||
signal("000852.SH")
|
|
||||||
|
|
||||||
rebalance.every_days(refresh_rate).at("10:18")
|
|
||||||
|
|
||||||
universe.exclude("paused", "st", "kcb", "one_yuan", "new_listing")
|
|
||||||
|
|
||||||
selection.limit(stocknum)
|
|
||||||
selection.market_cap_band(
|
|
||||||
field="market_cap",
|
|
||||||
lower=band_start(signal_close, base_index_level, xs, base_cap_floor),
|
|
||||||
upper=band_end(signal_close, base_index_level, xs, base_cap_ceiling)
|
|
||||||
)
|
|
||||||
|
|
||||||
risk.index_exposure(
|
|
||||||
signal_ma5 > signal_ma10 * rsi_rate ? 1.0 : trade_rate
|
|
||||||
)
|
|
||||||
|
|
||||||
filter.stock_expr(
|
|
||||||
stock_ma5 > stock_ma10 * rsi_rate &&
|
|
||||||
stock_ma10 > stock_ma30 * rsi_rate &&
|
|
||||||
rolling_mean("volume", 5) < rolling_mean("volume", 60)
|
|
||||||
)
|
|
||||||
|
|
||||||
risk.take_profit(close_rate)
|
|
||||||
risk.stop_loss(loss_rate)
|
|
||||||
allocation.buy_scale(touched_upper_limit ? 1.0 : trade_rate)
|
|
||||||
|
|
||||||
ordering.rank_by("market_cap", "asc")
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"strategyId": "microcap_volume_trend_000852",
|
|
||||||
"version": "2",
|
|
||||||
"parser": "omniquant-engine-script-v2",
|
|
||||||
"market": "CN_A",
|
|
||||||
"signalSymbol": "000852.SH",
|
|
||||||
"benchmark": {
|
|
||||||
"instrumentId": "000852.SH",
|
|
||||||
"fallbackInstrumentId": "000852.SH"
|
|
||||||
},
|
|
||||||
"engineConfig": {
|
|
||||||
"market": "CN_A",
|
|
||||||
"signalSymbol": "000852.SH",
|
|
||||||
"benchmarkSymbol": "000852.SH",
|
|
||||||
"refreshRate": 15,
|
|
||||||
"rankLimit": 40
|
|
||||||
},
|
|
||||||
"runtimeExpressions": {
|
|
||||||
"prelude": "let refresh_rate = 15;\nlet stocknum = 40;\nlet close_rate = 1.07;\nlet loss_rate = 0.93;\nlet rsi_rate = 1.0001;\nlet trade_rate = 0.5;\nlet xs = 4 / 500;\nlet base_index_level = 2000;\nlet base_cap_floor = 3;\nlet base_cap_ceiling = 28;\nfn band_start(current_price, base_index_level, xs, base_cap_floor) {\n if current_price == base_index_level {\n base_cap_floor\n } else if current_price > 0 {\n round((current_price - base_index_level) * xs + base_cap_floor)\n } else {\n base_cap_floor\n }\n}\nfn band_end(current_price, base_index_level, xs, base_cap_ceiling) {\n if current_price == base_index_level {\n base_cap_ceiling\n } else if current_price > 0 {\n round((current_price - base_index_level) * xs + base_cap_ceiling)\n } else {\n base_cap_ceiling\n }\n}",
|
|
||||||
"selection": {
|
|
||||||
"limitExpr": "stocknum",
|
|
||||||
"marketCapField": "market_cap",
|
|
||||||
"marketCapLowerExpr": "band_start(signal_close, base_index_level, xs, base_cap_floor)",
|
|
||||||
"marketCapUpperExpr": "band_end(signal_close, base_index_level, xs, base_cap_ceiling)",
|
|
||||||
"stockFilterExpr": "stock_ma5 > stock_ma10 * rsi_rate && stock_ma10 > stock_ma30 * rsi_rate && rolling_mean(\"volume\", 5) < rolling_mean(\"volume\", 60)"
|
|
||||||
},
|
|
||||||
"risk": {
|
|
||||||
"exposureExpr": "signal_ma5 > signal_ma10 * rsi_rate ? 1.0 : trade_rate",
|
|
||||||
"stopLossExpr": "loss_rate",
|
|
||||||
"takeProfitExpr": "close_rate"
|
|
||||||
},
|
|
||||||
"allocation": {
|
|
||||||
"buyScaleExpr": "touched_upper_limit ? 1.0 : trade_rate"
|
|
||||||
},
|
|
||||||
"ordering": {
|
|
||||||
"rankBy": "market_cap",
|
|
||||||
"rankExpr": "",
|
|
||||||
"rankOrder": "asc"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
let refresh_rate = 15;
|
|
||||||
let stocknum = 40;
|
|
||||||
let xs = 0.008;
|
|
||||||
let base_index_level = 2000;
|
|
||||||
let lower_offset = 3;
|
|
||||||
let upper_offset = 28;
|
|
||||||
|
|
||||||
fn cap_floor(current_price, base_index_level, xs, lower_offset) {
|
|
||||||
round((current_price - base_index_level) * xs + lower_offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cap_ceiling(current_price, base_index_level, xs, upper_offset) {
|
|
||||||
round((current_price - base_index_level) * xs + upper_offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
strategy("ai_generated_000001_open_cap_band") {
|
|
||||||
market("CN_A")
|
|
||||||
benchmark("000852.SH")
|
|
||||||
signal("000001.SH")
|
|
||||||
|
|
||||||
rebalance.every_days(refresh_rate).at("10:18")
|
|
||||||
|
|
||||||
universe.exclude("paused", "st", "kcb", "one_yuan", "new_listing")
|
|
||||||
|
|
||||||
selection.limit(stocknum)
|
|
||||||
selection.market_cap_band(
|
|
||||||
field="market_cap",
|
|
||||||
lower=cap_floor(signal_open, base_index_level, xs, lower_offset),
|
|
||||||
upper=cap_ceiling(signal_open, base_index_level, xs, upper_offset)
|
|
||||||
)
|
|
||||||
|
|
||||||
filter.stock_expr(
|
|
||||||
stock_ma5 > stock_ma10 &&
|
|
||||||
stock_ma10 > stock_ma30 &&
|
|
||||||
rolling_mean("volume", 5) < rolling_mean("volume", 60) &&
|
|
||||||
!ends_with(symbol, ".BJ") &&
|
|
||||||
!at_upper_limit &&
|
|
||||||
!at_lower_limit
|
|
||||||
)
|
|
||||||
|
|
||||||
ordering.rank_by("market_cap", "asc")
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"strategyId": "ai_generated_000001_open_cap_band",
|
|
||||||
"version": "2",
|
|
||||||
"parser": "omniquant-engine-script-v2",
|
|
||||||
"market": "CN_A",
|
|
||||||
"signalSymbol": "000001.SH",
|
|
||||||
"benchmark": {
|
|
||||||
"instrumentId": "000852.SH",
|
|
||||||
"fallbackInstrumentId": "000852.SH"
|
|
||||||
},
|
|
||||||
"engineConfig": {
|
|
||||||
"market": "CN_A",
|
|
||||||
"signalSymbol": "000001.SH",
|
|
||||||
"benchmarkSymbol": "000852.SH",
|
|
||||||
"refreshRate": 15,
|
|
||||||
"rankLimit": 40
|
|
||||||
},
|
|
||||||
"runtimeExpressions": {
|
|
||||||
"prelude": "let refresh_rate = 15;\nlet stocknum = 40;\nlet xs = 0.008;\nlet base_index_level = 2000;\nlet lower_offset = 3;\nlet upper_offset = 28;\n\nfn cap_floor(current_price, base_index_level, xs, lower_offset) {\nround((current_price - base_index_level) * xs + lower_offset)\n}\n\nfn cap_ceiling(current_price, base_index_level, xs, upper_offset) {\nround((current_price - base_index_level) * xs + upper_offset)\n}",
|
|
||||||
"selection": {
|
|
||||||
"limitExpr": "stocknum",
|
|
||||||
"marketCapField": "market_cap",
|
|
||||||
"marketCapLowerExpr": "cap_floor(signal_open, base_index_level, xs, lower_offset)",
|
|
||||||
"marketCapUpperExpr": "cap_ceiling(signal_open, base_index_level, xs, upper_offset)",
|
|
||||||
"stockFilterExpr": "stock_ma5 > stock_ma10 && stock_ma10 > stock_ma30 && rolling_mean(\"volume\", 5) < rolling_mean(\"volume\", 60) && !ends_with(symbol, \".BJ\") && !at_upper_limit && !at_lower_limit"
|
|
||||||
},
|
|
||||||
"ordering": {
|
|
||||||
"rankBy": "market_cap",
|
|
||||||
"rankExpr": "",
|
|
||||||
"rankOrder": "asc"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
date,benchmark,open,close,prev_close,volume
|
|
||||||
2024-01-02,CSI300.DEMO,2990,3000,2980,100000000
|
|
||||||
2024-01-03,CSI300.DEMO,3005,3020,3000,102000000
|
|
||||||
2024-01-04,CSI300.DEMO,3025,3050,3020,105000000
|
|
||||||
2024-01-05,CSI300.DEMO,3055,3080,3050,108000000
|
|
||||||
2024-01-08,CSI300.DEMO,3085,3110,3080,109000000
|
|
||||||
2024-01-09,CSI300.DEMO,3100,3090,3110,107000000
|
|
||||||
2024-01-10,CSI300.DEMO,3080,3040,3090,111000000
|
|
||||||
2024-01-11,CSI300.DEMO,3030,2990,3040,115000000
|
|
||||||
2024-01-12,CSI300.DEMO,2980,2950,2990,118000000
|
|
||||||
|
@@ -1,37 +0,0 @@
|
|||||||
date,symbol,is_st,is_new_listing,is_paused,allow_buy,allow_sell,is_kcb,is_one_yuan
|
|
||||||
2024-01-02,000001.SZ,false,true,false,false,true,false,false
|
|
||||||
2024-01-02,000002.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-02,000003.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-02,600001.SH,false,false,false,true,true,false,false
|
|
||||||
2024-01-03,000001.SZ,false,true,false,false,true,false,false
|
|
||||||
2024-01-03,000002.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-03,000003.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-03,600001.SH,false,false,false,true,true,false,false
|
|
||||||
2024-01-04,000001.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-04,000002.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-04,000003.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-04,600001.SH,false,false,false,true,true,false,false
|
|
||||||
2024-01-05,000001.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-05,000002.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-05,000003.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-05,600001.SH,false,false,false,true,true,false,false
|
|
||||||
2024-01-08,000001.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-08,000002.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-08,000003.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-08,600001.SH,false,false,false,true,true,false,false
|
|
||||||
2024-01-09,000001.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-09,000002.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-09,000003.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-09,600001.SH,false,false,false,true,true,false,false
|
|
||||||
2024-01-10,000001.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-10,000002.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-10,000003.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-10,600001.SH,false,false,false,true,true,false,false
|
|
||||||
2024-01-11,000001.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-11,000002.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-11,000003.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-11,600001.SH,false,false,true,false,false,false,false
|
|
||||||
2024-01-12,000001.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-12,000002.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-12,000003.SZ,false,false,false,true,true,false,false
|
|
||||||
2024-01-12,600001.SH,false,false,false,true,true,false,false
|
|
||||||
|
@@ -1,37 +0,0 @@
|
|||||||
date,symbol,market_cap_bn,free_float_cap_bn,pe_ttm
|
|
||||||
2024-01-02,000001.SZ,38,24,18
|
|
||||||
2024-01-02,000002.SZ,45,30,20
|
|
||||||
2024-01-02,000003.SZ,65,40,15
|
|
||||||
2024-01-02,600001.SH,85,55,13
|
|
||||||
2024-01-03,000001.SZ,39,24.5,18
|
|
||||||
2024-01-03,000002.SZ,46,30.5,20
|
|
||||||
2024-01-03,000003.SZ,64,39.5,15
|
|
||||||
2024-01-03,600001.SH,85,55,13
|
|
||||||
2024-01-04,000001.SZ,40,25,18
|
|
||||||
2024-01-04,000002.SZ,47,31,20
|
|
||||||
2024-01-04,000003.SZ,63,39,15
|
|
||||||
2024-01-04,600001.SH,86,55.5,13
|
|
||||||
2024-01-05,000001.SZ,41,25.5,18
|
|
||||||
2024-01-05,000002.SZ,48,32,20
|
|
||||||
2024-01-05,000003.SZ,62,38.5,15
|
|
||||||
2024-01-05,600001.SH,86,56,13
|
|
||||||
2024-01-08,000001.SZ,42,26,18
|
|
||||||
2024-01-08,000002.SZ,50,33,21
|
|
||||||
2024-01-08,000003.SZ,61,38,15
|
|
||||||
2024-01-08,600001.SH,87,56.5,13
|
|
||||||
2024-01-09,000001.SZ,44,27,19
|
|
||||||
2024-01-09,000002.SZ,52,34,21
|
|
||||||
2024-01-09,000003.SZ,60,37.5,15
|
|
||||||
2024-01-09,600001.SH,88,57,13
|
|
||||||
2024-01-10,000001.SZ,43,26.5,19
|
|
||||||
2024-01-10,000002.SZ,53,34.5,21
|
|
||||||
2024-01-10,000003.SZ,59,37,15
|
|
||||||
2024-01-10,600001.SH,89,57.5,13
|
|
||||||
2024-01-11,000001.SZ,42,26,18
|
|
||||||
2024-01-11,000002.SZ,52,34,21
|
|
||||||
2024-01-11,000003.SZ,58,36.5,15
|
|
||||||
2024-01-11,600001.SH,90,58,13
|
|
||||||
2024-01-12,000001.SZ,40,25,18
|
|
||||||
2024-01-12,000002.SZ,50,33,20
|
|
||||||
2024-01-12,000003.SZ,57,36,15
|
|
||||||
2024-01-12,600001.SH,92,59,13
|
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
symbol,name,board
|
|
||||||
000001.SZ,Alpha Components,Main
|
|
||||||
000002.SZ,Beta Precision,Main
|
|
||||||
000003.SZ,Charlie Materials,Main
|
|
||||||
600001.SH,Delta Industrials,Main
|
|
||||||
|
@@ -1,37 +0,0 @@
|
|||||||
date,symbol,open,high,low,close,prev_close,volume,paused
|
|
||||||
2024-01-02,000001.SZ,10.0,10.2,9.9,10.1,9.8,1200000,false
|
|
||||||
2024-01-02,000002.SZ,11.0,11.3,10.9,11.2,10.8,1100000,false
|
|
||||||
2024-01-02,000003.SZ,8.0,8.1,7.8,7.9,8.0,900000,false
|
|
||||||
2024-01-02,600001.SH,15.0,15.2,14.9,15.1,15.0,800000,false
|
|
||||||
2024-01-03,000001.SZ,10.2,10.5,10.1,10.4,10.1,1250000,false
|
|
||||||
2024-01-03,000002.SZ,11.2,11.6,11.1,11.5,11.2,1120000,false
|
|
||||||
2024-01-03,000003.SZ,7.8,7.9,7.3,7.4,7.9,930000,false
|
|
||||||
2024-01-03,600001.SH,15.1,15.3,15.0,15.2,15.1,820000,false
|
|
||||||
2024-01-04,000001.SZ,10.5,10.8,10.4,10.7,10.4,1280000,false
|
|
||||||
2024-01-04,000002.SZ,11.4,11.9,11.3,11.8,11.5,1150000,false
|
|
||||||
2024-01-04,000003.SZ,7.3,7.4,7.0,7.1,7.4,940000,false
|
|
||||||
2024-01-04,600001.SH,15.2,15.5,15.1,15.4,15.2,830000,false
|
|
||||||
2024-01-05,000001.SZ,10.8,11.1,10.7,11.0,10.7,1300000,false
|
|
||||||
2024-01-05,000002.SZ,11.9,12.1,11.8,12.0,11.8,1180000,false
|
|
||||||
2024-01-05,000003.SZ,7.0,7.1,6.8,6.9,7.1,950000,false
|
|
||||||
2024-01-05,600001.SH,15.4,15.6,15.3,15.5,15.4,840000,false
|
|
||||||
2024-01-08,000001.SZ,11.1,11.6,11.0,11.5,11.0,1400000,false
|
|
||||||
2024-01-08,000002.SZ,12.1,12.5,12.0,12.4,12.0,1200000,false
|
|
||||||
2024-01-08,000003.SZ,7.0,7.3,6.9,7.2,6.9,980000,false
|
|
||||||
2024-01-08,600001.SH,15.5,15.7,15.4,15.6,15.5,850000,false
|
|
||||||
2024-01-09,000001.SZ,11.6,12.4,11.5,12.3,11.5,1500000,false
|
|
||||||
2024-01-09,000002.SZ,12.5,12.9,12.4,12.8,12.4,1250000,false
|
|
||||||
2024-01-09,000003.SZ,7.2,7.5,7.1,7.4,7.2,990000,false
|
|
||||||
2024-01-09,600001.SH,15.6,15.7,15.4,15.5,15.6,860000,false
|
|
||||||
2024-01-10,000001.SZ,12.2,12.3,11.9,12.0,12.3,1450000,false
|
|
||||||
2024-01-10,000002.SZ,12.7,12.8,12.5,12.6,12.8,1220000,false
|
|
||||||
2024-01-10,000003.SZ,7.5,7.6,7.4,7.5,7.4,1000000,false
|
|
||||||
2024-01-10,600001.SH,15.4,15.5,15.1,15.2,15.5,870000,false
|
|
||||||
2024-01-11,000001.SZ,12.0,12.1,11.5,11.6,12.0,1420000,false
|
|
||||||
2024-01-11,000002.SZ,12.5,12.6,12.1,12.2,12.6,1210000,false
|
|
||||||
2024-01-11,000003.SZ,7.4,7.5,7.2,7.3,7.5,980000,false
|
|
||||||
2024-01-11,600001.SH,15.2,15.2,15.2,15.2,15.2,0,true
|
|
||||||
2024-01-12,000001.SZ,11.5,11.6,11.1,11.2,11.6,1380000,false
|
|
||||||
2024-01-12,000002.SZ,12.1,12.2,11.8,11.9,12.2,1190000,false
|
|
||||||
2024-01-12,000003.SZ,7.2,7.2,6.9,7.0,7.3,960000,false
|
|
||||||
2024-01-12,600001.SH,14.8,15.0,14.7,14.9,15.2,850000,false
|
|
||||||
|
@@ -52,22 +52,22 @@ futures path. Confirmed aligned areas:
|
|||||||
- [x] Rich explicit order styles exposed to platform scripts.
|
- [x] Rich explicit order styles exposed to platform scripts.
|
||||||
- [x] Minute-level `time_rule` semantics including market-open, market-close,
|
- [x] Minute-level `time_rule` semantics including market-open, market-close,
|
||||||
and physical-time style schedules.
|
and physical-time style schedules.
|
||||||
- [x] Fine-grained daily, minute, and tick strategy execution entrypoints.
|
- [x] Fine-grained daily and minute execution quote strategy entrypoints.
|
||||||
- [x] Scheduled actions evaluated against explicit intraday times.
|
- [x] Scheduled actions evaluated against explicit intraday times.
|
||||||
- [x] `update_universe`, `subscribe`, and `unsubscribe`.
|
- [x] `update_universe`, `subscribe`, and `unsubscribe`.
|
||||||
- [x] Tick-frequency subscription guards at strategy API level.
|
- [x] Intraday subscription guards at strategy API level; intraday execution uses minute quote semantics.
|
||||||
- [x] VWAP and TWAP explicit action styles.
|
- [x] VWAP and TWAP explicit action styles.
|
||||||
- [x] `order_target_portfolio_smart(..., order_prices=AlgoOrder, valuation_prices=...)`.
|
- [x] `order_target_portfolio_smart(..., order_prices=AlgoOrder, valuation_prices=...)`.
|
||||||
- [x] Trading PnL, position PnL, dividend receivable, and richer position
|
- [x] Trading PnL, position PnL, dividend receivable, and richer position
|
||||||
lifecycle fields.
|
lifecycle fields.
|
||||||
- [x] Stock position aliases including `order_book_id`, `avg_price`,
|
- [x] Stock position aliases including `order_book_id`, `avg_price`,
|
||||||
`sellable`, `closable`, `equity`, and `position_prev_close`.
|
`sellable`, `closable`, `equity`, and `position_prev_close`.
|
||||||
- [x] `history_bars` numeric helper for daily, intraday, and tick fields.
|
- [x] `history_bars` numeric helper for daily and minute execution quote fields.
|
||||||
- [x] `current_snapshot`, instrument metadata, all-instrument queries, and
|
- [x] `current_snapshot`, instrument metadata, all-instrument queries, and
|
||||||
active/historical instrument helpers.
|
active/historical instrument helpers.
|
||||||
- [x] Trading-date range, previous-date, and next-date helpers.
|
- [x] Trading-date range, previous-date, and next-date helpers.
|
||||||
- [x] Phase-aware minute/tick history cursor semantics matching the active bar
|
- [x] Phase-aware minute history cursor semantics matching the active bar or
|
||||||
or tick callback.
|
intraday execution quote callback.
|
||||||
- [x] Suspension, ST, date-range price, active instrument, and instrument
|
- [x] Suspension, ST, date-range price, active instrument, and instrument
|
||||||
history helpers.
|
history helpers.
|
||||||
- [x] Open-order status, unfilled quantity, final order lookup, average fill
|
- [x] Open-order status, unfilled quantity, final order lookup, average fill
|
||||||
|
|||||||
Executable
+164
@@ -0,0 +1,164 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
local message="$1"
|
||||||
|
local details="${2:-}"
|
||||||
|
printf '[FAIL] %s\n' "$message" >&2
|
||||||
|
if [[ -n "$details" ]]; then
|
||||||
|
printf '%s\n' "$details" >&2
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime_hits="$(
|
||||||
|
rg -n \
|
||||||
|
--glob '!**/.git/**' \
|
||||||
|
--glob '!**/target/**' \
|
||||||
|
--glob '!**/docs/**' \
|
||||||
|
--glob '!**/*.md' \
|
||||||
|
--glob '!**/tests/**' \
|
||||||
|
--glob '!**/*.test.rs' \
|
||||||
|
--glob '!**/*_test.rs' \
|
||||||
|
--glob '!crates/fidc-core/src/strategy_ai.rs' \
|
||||||
|
--glob '!scripts/verify-no-legacy-data-source.sh' \
|
||||||
|
'fidatacenter|FIDATACENTER|/v1/backtest/data|/v1/xuntou|ClickHouse|clickhouse|CLICKHOUSE|FIDC_BT_INSTRUMENT_METADATA_CSV|FIDC_BT_WRITE_SNAPSHOTS|FIDC_BT_WRITE_CSV_SNAPSHOTS|FIDC_BT_WRITE_COMBINED_SOURCE_ROW_CACHE|write_csv_snapshot_files|write_csv\(|FIDC_RISK_RUNTIME_FILE|FIDC_RISK_RUNTIME_URL|FIDC_FIRISK_RUNTIME_FILE|FiRisk runtime snapshot' \
|
||||||
|
crates Cargo.toml \
|
||||||
|
2>/dev/null || true
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -n "$runtime_hits" ]]; then
|
||||||
|
fail "legacy fidatacenter/ClickHouse/CSV snapshot/FiRisk runtime data source references are not allowed in fidc-backtest-engine" "$runtime_hits"
|
||||||
|
fi
|
||||||
|
|
||||||
|
manifest_hits="$(
|
||||||
|
rg -n --glob '!scripts/verify-no-legacy-data-source.sh' '\b(mysql|mariadb|clickhouse|clickhouse-rs|mysql_async|sqlx-mysql)\b' Cargo.toml crates 2>/dev/null || true
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -n "$manifest_hits" ]]; then
|
||||||
|
fail "legacy database dependencies are not allowed in fidc-backtest-engine manifests" "$manifest_hits"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local_only_hits="$(
|
||||||
|
rg -n \
|
||||||
|
--glob '!**/.git/**' \
|
||||||
|
--glob '!**/target/**' \
|
||||||
|
--glob '!**/docs/**' \
|
||||||
|
--glob '!**/*.md' \
|
||||||
|
--glob '!**/tests/**' \
|
||||||
|
--glob '!**/*.test.rs' \
|
||||||
|
--glob '!**/*_test.rs' \
|
||||||
|
--glob '!crates/fidc-core/src/strategy_ai.rs' \
|
||||||
|
--glob '!scripts/verify-no-legacy-data-source.sh' \
|
||||||
|
'FICLAW_DATA_AGENT_URL|ficlaw_data\.source_rows_v1|strategy-factory-source-lake://local|FIDC_BT_WRITE_COMBINED_SOURCE_ROW_CACHE' \
|
||||||
|
crates Cargo.toml \
|
||||||
|
2>/dev/null || true
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -n "$local_only_hits" ]]; then
|
||||||
|
fail "legacy or local-only data source references are not allowed in fidc-backtest-engine runtime" "$local_only_hits"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_query_hits="$(
|
||||||
|
rg -n \
|
||||||
|
--glob '!**/.git/**' \
|
||||||
|
--glob '!**/target/**' \
|
||||||
|
--glob '!**/docs/**' \
|
||||||
|
--glob '!**/*.md' \
|
||||||
|
--glob '!**/tests/**' \
|
||||||
|
--glob '!**/*.test.rs' \
|
||||||
|
--glob '!**/*_test.rs' \
|
||||||
|
--glob '!crates/fidc-core/src/strategy_ai.rs' \
|
||||||
|
--glob '!scripts/verify-no-legacy-data-source.sh' \
|
||||||
|
'/v1/query/(source-rows|daily-execution-prices|minute-execution-prices|instruments|corporate-actions)\.json' \
|
||||||
|
crates Cargo.toml \
|
||||||
|
2>/dev/null || true
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -n "$json_query_hits" ]]; then
|
||||||
|
fail "JSON Source Lake high-throughput endpoints are not allowed in fidc-backtest-engine runtime; use Arrow endpoints" "$json_query_hits"
|
||||||
|
fi
|
||||||
|
|
||||||
|
fused_hits="$(
|
||||||
|
rg -n \
|
||||||
|
--glob '!**/.git/**' \
|
||||||
|
--glob '!**/target/**' \
|
||||||
|
--glob '!**/docs/**' \
|
||||||
|
--glob '!**/*.md' \
|
||||||
|
--glob '!**/tests/**' \
|
||||||
|
--glob '!**/*.test.rs' \
|
||||||
|
--glob '!**/*_test.rs' \
|
||||||
|
--glob '!crates/fidc-core/src/strategy_ai.rs' \
|
||||||
|
--glob '!scripts/verify-no-legacy-data-source.sh' \
|
||||||
|
'exported_fused|fidc_fused|fusion|fused|wide_table|wide table|source_rows_export|exported_daily|merged_daily|daily_merged|merged_source|materialized[_ -]source|materialized_source_rows|source_rows_materialized|融合表|融合宽表' \
|
||||||
|
crates Cargo.toml \
|
||||||
|
2>/dev/null || true
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -n "$fused_hits" ]]; then
|
||||||
|
fail "exported fused tables are not allowed in fidc-backtest-engine runtime; use Strategy Factory Source Lake source rows directly" "$fused_hits"
|
||||||
|
fi
|
||||||
|
|
||||||
|
feature_store_hits="$(
|
||||||
|
rg -n \
|
||||||
|
--glob '!**/.git/**' \
|
||||||
|
--glob '!**/target/**' \
|
||||||
|
--glob '!**/docs/**' \
|
||||||
|
--glob '!**/*.md' \
|
||||||
|
--glob '!**/tests/**' \
|
||||||
|
--glob '!**/*.test.rs' \
|
||||||
|
--glob '!**/*_test.rs' \
|
||||||
|
--glob '!crates/fidc-core/src/strategy_ai.rs' \
|
||||||
|
--glob '!scripts/verify-no-legacy-data-source.sh' \
|
||||||
|
'research_feature_store|feature_store|FEATURE_STORE|daily_minute_current|FIDC_STRATEGY_FACTORY_ENABLE_FEATURE_STORE_CACHE|ALPHA_FACTORY_ENABLE_FEATURE_STORE_CACHE|ENABLE_FEATURE_STORE_CACHE' \
|
||||||
|
crates Cargo.toml \
|
||||||
|
2>/dev/null || true
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -n "$feature_store_hits" ]]; then
|
||||||
|
fail "historical feature-store paths are not allowed in fidc-backtest-engine runtime; use Strategy Factory Source Lake raw/indicator/artifact partitions and discardable caches" "$feature_store_hits"
|
||||||
|
fi
|
||||||
|
|
||||||
|
truth_csv_hits="$(
|
||||||
|
rg -n \
|
||||||
|
--glob '!**/.git/**' \
|
||||||
|
--glob '!**/target/**' \
|
||||||
|
--glob '!**/docs/**' \
|
||||||
|
--glob '!**/*.md' \
|
||||||
|
--glob '!**/tests/**' \
|
||||||
|
--glob '!**/*.test.rs' \
|
||||||
|
--glob '!**/*_test.rs' \
|
||||||
|
--glob '!crates/fidc-core/src/strategy_ai.rs' \
|
||||||
|
--glob '!scripts/verify-no-legacy-data-source.sh' \
|
||||||
|
'FIDC_BT_TRUTH_STOCK_LIST_CSV|OMNI_BT_TRUTH_STOCK_LIST_CSV|OMNI_BACKTEST_TRUTH_STOCK_LIST_CSV|selection_source=truth_csv|truth_stock_list|truth_csv' \
|
||||||
|
crates Cargo.toml \
|
||||||
|
2>/dev/null || true
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -n "$truth_csv_hits" ]]; then
|
||||||
|
fail "CSV truth stock-list overrides are not allowed in fidc-backtest-engine runtime; use Source Lake runtime spec selection only" "$truth_csv_hits"
|
||||||
|
fi
|
||||||
|
|
||||||
|
csv_snapshot_loader_hits="$(
|
||||||
|
rg -n \
|
||||||
|
--glob '!**/.git/**' \
|
||||||
|
--glob '!**/target/**' \
|
||||||
|
--glob '!**/docs/**' \
|
||||||
|
--glob '!**/*.md' \
|
||||||
|
--glob '!**/tests/**' \
|
||||||
|
--glob '!**/*.test.rs' \
|
||||||
|
--glob '!**/*_test.rs' \
|
||||||
|
--glob '!scripts/verify-no-legacy-data-source.sh' \
|
||||||
|
'from_csv_dir|from_partitioned_dir|instruments\.csv|candidate_flags\.csv|market\.csv|benchmark\.csv' \
|
||||||
|
crates Cargo.toml \
|
||||||
|
2>/dev/null || true
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -n "$csv_snapshot_loader_hits" ]]; then
|
||||||
|
fail "CSV snapshot loaders are not allowed in fidc-backtest-engine runtime; construct DataSet from Source Lake components" "$csv_snapshot_loader_hits"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '[OK] fidc-backtest-engine has no legacy runtime data-source references\n'
|
||||||
Executable
+61
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
PYTHON_BIN="${PYTHON_BIN:-python3}"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
local message="$1"
|
||||||
|
local details="${2:-}"
|
||||||
|
printf '[FAIL] %s\n' "$message" >&2
|
||||||
|
if [[ -n "$details" ]]; then
|
||||||
|
printf '%s\n' "$details" >&2
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
run_core_test() {
|
||||||
|
local filter="$1"
|
||||||
|
local tmp
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
printf '[INFO] cargo test -p fidc-core %s\n' "$filter"
|
||||||
|
if cargo test -p fidc-core "$filter" -- --nocapture 2>&1 | tee "$tmp"; then
|
||||||
|
local passed_count
|
||||||
|
passed_count="$(
|
||||||
|
"$PYTHON_BIN" - "$tmp" <<'PY'
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for line in open(sys.argv[1], encoding="utf-8", errors="replace"):
|
||||||
|
match = re.search(r"test result: ok\. (\d+) passed;", line)
|
||||||
|
if match:
|
||||||
|
count += int(match.group(1))
|
||||||
|
print(count)
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
rm -f "$tmp"
|
||||||
|
if [[ "$passed_count" -le 0 ]]; then
|
||||||
|
fail "cargo test filter matched 0 tests: package=fidc-core filter=${filter}"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local output
|
||||||
|
output="$(cat "$tmp")"
|
||||||
|
rm -f "$tmp"
|
||||||
|
fail "cargo test failed: package=fidc-core filter=${filter}" "$output"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_core_test eligible_universe_does_not_require_candidate_risk_state_when_selection_risk_is_disabled
|
||||||
|
run_core_test next_bar_open_eligible_universe_helper_does_not_block_on_decision_day_risk
|
||||||
|
run_core_test platform_next_open_selection_records_risk_diagnostics_without_filtering
|
||||||
|
run_core_test next_open_buy_risk_uses_execution_date_not_signal_date
|
||||||
|
run_core_test next_open_buy_limit_risk_uses_open_not_close
|
||||||
|
run_core_test next_open_sell_risk_uses_execution_date_not_signal_date
|
||||||
|
run_core_test next_open_sell_limit_risk_uses_open_not_close
|
||||||
|
run_core_test next_bar_open_sell_respects_allow_sell_policy_on_execution_day
|
||||||
|
run_core_test volume_limit_uses_floor_for_odd_lot_sell
|
||||||
|
run_core_test configurable_upper_limit_buy_filter_can_be_disabled
|
||||||
|
|
||||||
|
printf '[OK] fidc-backtest-engine runtime risk contracts passed\n'
|
||||||
Reference in New Issue
Block a user