feat(portfolio): 组合回测接入A股费用(佣金/印花税/最低佣金/滑点)对齐个股回测; BulletTrade set_order_cost/set_slippage [vps]
This commit is contained in:
@@ -8,6 +8,11 @@ export interface PortfolioBacktestReq {
|
||||
end_date: string
|
||||
initial_cash: number
|
||||
benchmark?: string
|
||||
// A 股费用(对齐个股回测)
|
||||
commission_rate?: number
|
||||
stamp_duty_rate?: number
|
||||
min_commission?: number
|
||||
slippage?: number
|
||||
}
|
||||
|
||||
export interface EquityPoint {
|
||||
|
||||
@@ -57,6 +57,11 @@ const form = reactive({
|
||||
end: dr.end,
|
||||
cash: 1_000_000,
|
||||
benchmark: '000300.XSHG',
|
||||
// A 股费用(对齐个股回测)
|
||||
commission_rate: 0.0003,
|
||||
stamp_duty_rate: 0.001,
|
||||
min_commission: 5,
|
||||
slippage: 0.001,
|
||||
})
|
||||
watch(
|
||||
() => [form.start, form.end],
|
||||
@@ -199,6 +204,10 @@ async function onSubmit(): Promise<void> {
|
||||
end_date: form.end,
|
||||
initial_cash: form.cash,
|
||||
benchmark: form.benchmark,
|
||||
commission_rate: Number(form.commission_rate),
|
||||
stamp_duty_rate: Number(form.stamp_duty_rate),
|
||||
min_commission: Number(form.min_commission),
|
||||
slippage: Number(form.slippage),
|
||||
})
|
||||
try {
|
||||
localStorage.setItem('last_portfolio_task', tid)
|
||||
@@ -328,6 +337,22 @@ function fmtNum(v: number | null | undefined, digits = 2): string {
|
||||
<el-option v-for="b in BENCHMARK_OPTIONS" :key="b.value" :label="b.label" :value="b.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="佣金率">
|
||||
<el-input v-model="form.commission_rate" style="width: 160px" />
|
||||
<span class="muted form-hint">0.0003=万3(双边,最低5元)</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="印花税率">
|
||||
<el-input v-model="form.stamp_duty_rate" style="width: 160px" />
|
||||
<span class="muted form-hint">0.001=千1(仅卖出)</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="最低佣金">
|
||||
<el-input v-model="form.min_commission" style="width: 160px" />
|
||||
<span class="muted form-hint">元/笔</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="滑点(比率)">
|
||||
<el-input v-model="form.slippage" style="width: 160px" />
|
||||
<span class="muted form-hint">0.001=万10,0=不加</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="submit-bar">
|
||||
<el-button type="primary" size="large" @click="onSubmit">开始回测</el-button>
|
||||
|
||||
@@ -38,6 +38,11 @@ class PortfolioBacktestRequest(BaseModel):
|
||||
description="策略: all_weather/momentum_timing/value_selection/small_cap",
|
||||
)
|
||||
max_pool: int = Field(default=30, description="选股池上限: 0=全市场不限, N=前N只(MVP验证用)")
|
||||
# A 股费用(对齐个股回测)
|
||||
commission_rate: float = Field(default=0.0003, description="佣金率双边(万3=0.0003)")
|
||||
stamp_duty_rate: float = Field(default=0.001, description="印花税率卖出(千1=0.001)")
|
||||
min_commission: float = Field(default=5.0, description="单笔最低佣金(元)")
|
||||
slippage: float = Field(default=0.0, description="滑点比率(万10=0.001,0=不加)")
|
||||
|
||||
|
||||
@router.post("/portfolio/backtest", dependencies=[Depends(verify_token)])
|
||||
@@ -51,6 +56,10 @@ async def run_portfolio_backtest(req: PortfolioBacktestRequest):
|
||||
strategy=req.strategy,
|
||||
max_pool=req.max_pool,
|
||||
provider_config=None,
|
||||
commission_rate=req.commission_rate,
|
||||
stamp_duty_rate=req.stamp_duty_rate,
|
||||
min_commission=req.min_commission,
|
||||
slippage=req.slippage,
|
||||
)
|
||||
return {"task_id": tid}
|
||||
|
||||
|
||||
@@ -60,7 +60,11 @@ def run_portfolio_task(spec: dict) -> Any:
|
||||
db_path = spec.get("db_path", "")
|
||||
file_dir = spec.get("file_dir")
|
||||
|
||||
argv, cwd = _build_argv(start, end, cash, benchmark, max_pool, provider_config, strategy)
|
||||
argv, cwd = _build_argv(
|
||||
start, end, cash, benchmark, max_pool, provider_config, strategy,
|
||||
spec.get("commission_rate", 0.0003), spec.get("stamp_duty_rate", 0.001),
|
||||
spec.get("min_commission", 5.0), spec.get("slippage", 0.0),
|
||||
)
|
||||
logger.info("[portfolio_worker] task=%s running: %s", task_id, " ".join(argv[3:]))
|
||||
|
||||
try:
|
||||
@@ -130,6 +134,8 @@ def _build_argv(
|
||||
start: str, end: str, cash: float, benchmark: str,
|
||||
max_pool: int, provider_config: Optional[dict],
|
||||
strategy: str = "all_weather",
|
||||
commission_rate: float = 0.0003, stamp_duty_rate: float = 0.001,
|
||||
min_commission: float = 5.0, slippage: float = 0.0,
|
||||
) -> tuple[list[str], Optional[str]]:
|
||||
"""Three-machine adaptive argv construction.
|
||||
|
||||
@@ -146,6 +152,10 @@ def _build_argv(
|
||||
"--cash", str(cash), "--benchmark", benchmark,
|
||||
"--strategy", strategy,
|
||||
"--max-pool", str(max_pool),
|
||||
"--commission-rate", str(commission_rate),
|
||||
"--stamp-duty-rate", str(stamp_duty_rate),
|
||||
"--min-commission", str(min_commission),
|
||||
"--slippage", str(slippage),
|
||||
]
|
||||
# NAS container: unified provider reads NAS authoritative data layer
|
||||
# (dbbardata + parquet). VPS keeps default provider (cwd=_VPS_WORKDIR).
|
||||
@@ -168,7 +178,9 @@ def _build_argv(
|
||||
f"--start {start} --end {end} "
|
||||
f"--cash {cash} --benchmark {benchmark} "
|
||||
f"--strategy {strategy} "
|
||||
f"--max-pool {max_pool}"
|
||||
f"--max-pool {max_pool} "
|
||||
f"--commission-rate {commission_rate} --stamp-duty-rate {stamp_duty_rate} "
|
||||
f"--min-commission {min_commission} --slippage {slippage}"
|
||||
)
|
||||
ssh_argv = [
|
||||
"ssh", "-o", "ConnectTimeout=15", "-o", "StrictHostKeyChecking=no",
|
||||
|
||||
@@ -124,7 +124,11 @@ class Orchestrator:
|
||||
async def submit_portfolio(self, start: str, end: str, cash: float,
|
||||
benchmark: str, strategy: str = "all_weather",
|
||||
max_pool: int = 30,
|
||||
provider_config=None) -> str:
|
||||
provider_config=None,
|
||||
commission_rate: float = 0.0003,
|
||||
stamp_duty_rate: float = 0.001,
|
||||
min_commission: float = 5.0,
|
||||
slippage: float = 0.0) -> str:
|
||||
"""Submit a portfolio backtest task asynchronously.
|
||||
|
||||
Runs runner_backtest as a subprocess (3600s hard cap) inside the
|
||||
@@ -143,6 +147,10 @@ class Orchestrator:
|
||||
strategy=strategy,
|
||||
max_pool=max_pool,
|
||||
provider_config=provider_config,
|
||||
commission_rate=commission_rate,
|
||||
stamp_duty_rate=stamp_duty_rate,
|
||||
min_commission=min_commission,
|
||||
slippage=slippage,
|
||||
db_path=self.db_path,
|
||||
file_dir=file_dir,
|
||||
)
|
||||
|
||||
@@ -49,6 +49,11 @@ def parse_args() -> argparse.Namespace:
|
||||
p.add_argument("--benchmark", default="000300.XSHG", help="基准代码")
|
||||
p.add_argument("--max-pool", type=int, default=0, help="限制选股池前N只(0=不限,MVP验证用)")
|
||||
p.add_argument("--frequency", default="day", help="回测频率 day/minute")
|
||||
# A 股费用(对齐个股回测;BulletTrade 默认仅印花税千1+最低5元,这里显式可配)
|
||||
p.add_argument("--commission-rate", type=float, default=0.0003, help="佣金率双边(万3=0.0003)")
|
||||
p.add_argument("--stamp-duty-rate", type=float, default=0.001, help="印花税率卖出(千1=0.001)")
|
||||
p.add_argument("--min-commission", type=float, default=5.0, help="单笔最低佣金(元)")
|
||||
p.add_argument("--slippage", type=float, default=0.0, help="滑点比率(万10=0.001,0=不加)")
|
||||
p.add_argument(
|
||||
"--strategy", default="all_weather",
|
||||
choices=["all_weather", "momentum_timing", "value_selection", "small_cap"],
|
||||
@@ -237,6 +242,21 @@ def run_backtest(args: argparse.Namespace) -> Dict[str, Any]:
|
||||
# 才能真正设到 bullet_trade settings → fq_mode=pre 与 get_current_data 一致, 买入才成交
|
||||
holder["broker"] = build_broker_facade_inner(strategy, context)
|
||||
strategy.broker = holder["broker"]
|
||||
# A 股费用 + 滑点(聚宽风格全局函数,对齐个股回测;BulletTrade 默认费用不全)
|
||||
from bullet_trade.core.api import set_order_cost, set_slippage # type: ignore
|
||||
from bullet_trade.core.settings import OrderCost, FixedSlippage # type: ignore
|
||||
set_order_cost(
|
||||
OrderCost(
|
||||
open_tax=0.0,
|
||||
close_tax=args.stamp_duty_rate,
|
||||
open_commission=args.commission_rate,
|
||||
close_commission=args.commission_rate,
|
||||
min_commission=args.min_commission,
|
||||
),
|
||||
type="stock",
|
||||
)
|
||||
if args.slippage:
|
||||
set_slippage(FixedSlippage(value=args.slippage))
|
||||
strategy.initialize(context)
|
||||
|
||||
def build_broker_facade_inner(strategy: Any, context: Any):
|
||||
|
||||
Reference in New Issue
Block a user