feat(portfolio): strategy 字段全链透传(API→orchestrator→worker→runner_backtest + 前端选择器)
CI/CD / test (push) Successful in 10s
CI/CD / nas-deploy (push) Successful in 20s
CI/CD / nas-verify (push) Successful in 3s

后端: PortfolioBacktestRequest.strategy → submit_portfolio(strategy=) → spec →
portfolio_worker _build_argv --strategy(NAS local + Mac SSH 两分支)
前端: PortfolioBacktestReq.strategy? + 策略下拉(4选项) + 副标题动态 + onSubmit 透传
runner_backtest CLI 已支持 --strategy(choices 4 策略),本提交不动
This commit is contained in:
2026-08-01 23:01:26 +08:00
parent 5c5bd9d7bb
commit 7a14d7da82
5 changed files with 40 additions and 4 deletions
+1
View File
@@ -2,6 +2,7 @@ import { apiClient } from './client'
export interface PortfolioBacktestReq {
pool: string
strategy?: string
start_date: string
end_date: string
initial_cash: number
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { ref, reactive, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import type { EChartsCoreOption } from 'echarts'
import { useChart } from '@/composables/useChart'
@@ -28,6 +28,7 @@ let pollTimer: ReturnType<typeof setInterval> | null = null
const form = reactive({
pool: 'hs300_subset',
strategy: 'all_weather',
start: '2024-01-01',
end: '2024-02-29',
cash: 1_000_000,
@@ -39,6 +40,17 @@ const poolOptions = [
{ label: '全市场(慢,非 MVP)', value: 'all' },
]
const strategyOptions = [
{ label: '全天候轮动', value: 'all_weather' },
{ label: '牛熊动量', value: 'momentum_timing' },
{ label: '价值精选', value: 'value_selection' },
{ label: '小市值轮动', value: 'small_cap' },
]
const strategyLabel = computed(
() => strategyOptions.find((o) => o.value === form.strategy)?.label ?? form.strategy,
)
// 净值曲线
const equityEl = ref<HTMLDivElement>()
const { setOption: setEquityOption } = useChart(equityEl)
@@ -95,6 +107,7 @@ async function onSubmit(): Promise<void> {
try {
const tid = await postPortfolioBacktest({
pool: form.pool,
strategy: form.strategy,
start_date: form.start,
end_date: form.end,
initial_cash: form.cash,
@@ -150,7 +163,7 @@ async function onSubmit(): Promise<void> {
<div>
<h2 class="page-title">组合策略回测</h2>
<p class="page-subtitle">
BulletTrade + 全天候轮动策略 · 本地执行回测 · MVP 验证链路
BulletTrade + {{ strategyLabel }} · 本地执行回测 · MVP 验证链路
</p>
</div>
</div>
@@ -169,6 +182,17 @@ async function onSubmit(): Promise<void> {
</el-select>
<span class="muted form-hint">MVP 默认 HS300 子集(20-30 ),快速验证链路</span>
</el-form-item>
<el-form-item label="策略">
<el-select v-model="form.strategy" style="width: 320px">
<el-option
v-for="opt in strategyOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<span class="muted form-hint">选择回测策略</span>
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker
v-model="form.start"
+5
View File
@@ -33,6 +33,10 @@ class PortfolioBacktestRequest(BaseModel):
end_date: str = Field(default="2024-02-29", description="YYYY-MM-DD")
initial_cash: float = Field(default=1_000_000.0, description="初始资金(元)")
benchmark: str = Field(default="000300.XSHG", description="基准代码")
strategy: str = Field(
default="all_weather",
description="策略: all_weather/momentum_timing/value_selection/small_cap",
)
@router.post("/portfolio/backtest", dependencies=[Depends(verify_token)])
@@ -43,6 +47,7 @@ async def run_portfolio_backtest(req: PortfolioBacktestRequest):
end=req.end_date,
cash=req.initial_cash,
benchmark=req.benchmark,
strategy=req.strategy,
max_pool=30,
provider_config=None,
)
+5 -1
View File
@@ -47,12 +47,13 @@ def run_portfolio_task(spec: dict) -> Any:
end = spec["end"]
cash = spec["cash"]
benchmark = spec["benchmark"]
strategy = spec.get("strategy", "all_weather")
max_pool = spec["max_pool"]
provider_config = spec.get("provider_config")
db_path = spec.get("db_path", "")
file_dir = spec.get("file_dir")
argv, cwd = _build_argv(start, end, cash, benchmark, max_pool, provider_config)
argv, cwd = _build_argv(start, end, cash, benchmark, max_pool, provider_config, strategy)
logger.info("[portfolio_worker] task=%s running: %s", task_id, " ".join(argv[3:]))
proc = subprocess.run(
@@ -108,6 +109,7 @@ def run_portfolio_task(spec: dict) -> Any:
def _build_argv(
start: str, end: str, cash: float, benchmark: str,
max_pool: int, provider_config: Optional[dict],
strategy: str = "all_weather",
) -> tuple[list[str], Optional[str]]:
"""Three-machine adaptive argv construction.
@@ -122,6 +124,7 @@ def _build_argv(
sys.executable, "-X", "utf8", "-m", "sanguo_portfolio.runner_backtest",
"--json", "--start", start, "--end", end,
"--cash", str(cash), "--benchmark", benchmark,
"--strategy", strategy,
"--max-pool", str(max_pool),
]
# NAS container: unified provider reads NAS authoritative data layer
@@ -144,6 +147,7 @@ def _build_argv(
f"{_VPS_PYTHON} -X utf8 -m sanguo_portfolio.runner_backtest --json "
f"--start {start} --end {end} "
f"--cash {cash} --benchmark {benchmark} "
f"--strategy {strategy} "
f"--max-pool {max_pool}"
)
ssh_argv = [
+3 -1
View File
@@ -122,7 +122,8 @@ class Orchestrator:
return task_id
async def submit_portfolio(self, start: str, end: str, cash: float,
benchmark: str, max_pool: int = 30,
benchmark: str, strategy: str = "all_weather",
max_pool: int = 30,
provider_config=None) -> str:
"""Submit a portfolio backtest task asynchronously.
@@ -139,6 +140,7 @@ class Orchestrator:
end=end,
cash=cash,
benchmark=benchmark,
strategy=strategy,
max_pool=max_pool,
provider_config=provider_config,
db_path=self.db_path,