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
|
end_date: string
|
||||||
initial_cash: number
|
initial_cash: number
|
||||||
benchmark?: string
|
benchmark?: string
|
||||||
|
// A 股费用(对齐个股回测)
|
||||||
|
commission_rate?: number
|
||||||
|
stamp_duty_rate?: number
|
||||||
|
min_commission?: number
|
||||||
|
slippage?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EquityPoint {
|
export interface EquityPoint {
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ const form = reactive({
|
|||||||
end: dr.end,
|
end: dr.end,
|
||||||
cash: 1_000_000,
|
cash: 1_000_000,
|
||||||
benchmark: '000300.XSHG',
|
benchmark: '000300.XSHG',
|
||||||
|
// A 股费用(对齐个股回测)
|
||||||
|
commission_rate: 0.0003,
|
||||||
|
stamp_duty_rate: 0.001,
|
||||||
|
min_commission: 5,
|
||||||
|
slippage: 0.001,
|
||||||
})
|
})
|
||||||
watch(
|
watch(
|
||||||
() => [form.start, form.end],
|
() => [form.start, form.end],
|
||||||
@@ -199,6 +204,10 @@ async function onSubmit(): Promise<void> {
|
|||||||
end_date: form.end,
|
end_date: form.end,
|
||||||
initial_cash: form.cash,
|
initial_cash: form.cash,
|
||||||
benchmark: form.benchmark,
|
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 {
|
try {
|
||||||
localStorage.setItem('last_portfolio_task', tid)
|
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-option v-for="b in BENCHMARK_OPTIONS" :key="b.value" :label="b.label" :value="b.value" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</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>
|
</el-form>
|
||||||
<div class="submit-bar">
|
<div class="submit-bar">
|
||||||
<el-button type="primary" size="large" @click="onSubmit">开始回测</el-button>
|
<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",
|
description="策略: all_weather/momentum_timing/value_selection/small_cap",
|
||||||
)
|
)
|
||||||
max_pool: int = Field(default=30, description="选股池上限: 0=全市场不限, N=前N只(MVP验证用)")
|
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)])
|
@router.post("/portfolio/backtest", dependencies=[Depends(verify_token)])
|
||||||
@@ -51,6 +56,10 @@ async def run_portfolio_backtest(req: PortfolioBacktestRequest):
|
|||||||
strategy=req.strategy,
|
strategy=req.strategy,
|
||||||
max_pool=req.max_pool,
|
max_pool=req.max_pool,
|
||||||
provider_config=None,
|
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}
|
return {"task_id": tid}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,11 @@ def run_portfolio_task(spec: dict) -> Any:
|
|||||||
db_path = spec.get("db_path", "")
|
db_path = spec.get("db_path", "")
|
||||||
file_dir = spec.get("file_dir")
|
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:]))
|
logger.info("[portfolio_worker] task=%s running: %s", task_id, " ".join(argv[3:]))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -130,6 +134,8 @@ def _build_argv(
|
|||||||
start: str, end: str, cash: float, benchmark: str,
|
start: str, end: str, cash: float, benchmark: str,
|
||||||
max_pool: int, provider_config: Optional[dict],
|
max_pool: int, provider_config: Optional[dict],
|
||||||
strategy: str = "all_weather",
|
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]]:
|
) -> tuple[list[str], Optional[str]]:
|
||||||
"""Three-machine adaptive argv construction.
|
"""Three-machine adaptive argv construction.
|
||||||
|
|
||||||
@@ -146,6 +152,10 @@ def _build_argv(
|
|||||||
"--cash", str(cash), "--benchmark", benchmark,
|
"--cash", str(cash), "--benchmark", benchmark,
|
||||||
"--strategy", strategy,
|
"--strategy", strategy,
|
||||||
"--max-pool", str(max_pool),
|
"--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
|
# NAS container: unified provider reads NAS authoritative data layer
|
||||||
# (dbbardata + parquet). VPS keeps default provider (cwd=_VPS_WORKDIR).
|
# (dbbardata + parquet). VPS keeps default provider (cwd=_VPS_WORKDIR).
|
||||||
@@ -168,7 +178,9 @@ def _build_argv(
|
|||||||
f"--start {start} --end {end} "
|
f"--start {start} --end {end} "
|
||||||
f"--cash {cash} --benchmark {benchmark} "
|
f"--cash {cash} --benchmark {benchmark} "
|
||||||
f"--strategy {strategy} "
|
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_argv = [
|
||||||
"ssh", "-o", "ConnectTimeout=15", "-o", "StrictHostKeyChecking=no",
|
"ssh", "-o", "ConnectTimeout=15", "-o", "StrictHostKeyChecking=no",
|
||||||
|
|||||||
@@ -124,7 +124,11 @@ class Orchestrator:
|
|||||||
async def submit_portfolio(self, start: str, end: str, cash: float,
|
async def submit_portfolio(self, start: str, end: str, cash: float,
|
||||||
benchmark: str, strategy: str = "all_weather",
|
benchmark: str, strategy: str = "all_weather",
|
||||||
max_pool: int = 30,
|
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.
|
"""Submit a portfolio backtest task asynchronously.
|
||||||
|
|
||||||
Runs runner_backtest as a subprocess (3600s hard cap) inside the
|
Runs runner_backtest as a subprocess (3600s hard cap) inside the
|
||||||
@@ -143,6 +147,10 @@ class Orchestrator:
|
|||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
max_pool=max_pool,
|
max_pool=max_pool,
|
||||||
provider_config=provider_config,
|
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,
|
db_path=self.db_path,
|
||||||
file_dir=file_dir,
|
file_dir=file_dir,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ def parse_args() -> argparse.Namespace:
|
|||||||
p.add_argument("--benchmark", default="000300.XSHG", help="基准代码")
|
p.add_argument("--benchmark", default="000300.XSHG", help="基准代码")
|
||||||
p.add_argument("--max-pool", type=int, default=0, help="限制选股池前N只(0=不限,MVP验证用)")
|
p.add_argument("--max-pool", type=int, default=0, help="限制选股池前N只(0=不限,MVP验证用)")
|
||||||
p.add_argument("--frequency", default="day", help="回测频率 day/minute")
|
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(
|
p.add_argument(
|
||||||
"--strategy", default="all_weather",
|
"--strategy", default="all_weather",
|
||||||
choices=["all_weather", "momentum_timing", "value_selection", "small_cap"],
|
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 一致, 买入才成交
|
# 才能真正设到 bullet_trade settings → fq_mode=pre 与 get_current_data 一致, 买入才成交
|
||||||
holder["broker"] = build_broker_facade_inner(strategy, context)
|
holder["broker"] = build_broker_facade_inner(strategy, context)
|
||||||
strategy.broker = holder["broker"]
|
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)
|
strategy.initialize(context)
|
||||||
|
|
||||||
def build_broker_facade_inner(strategy: Any, context: Any):
|
def build_broker_facade_inner(strategy: Any, context: Any):
|
||||||
|
|||||||
Reference in New Issue
Block a user