feat(portfolio): strategy 字段全链透传(API→orchestrator→worker→runner_backtest + 前端选择器)
后端: 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:
@@ -2,6 +2,7 @@ import { apiClient } from './client'
|
|||||||
|
|
||||||
export interface PortfolioBacktestReq {
|
export interface PortfolioBacktestReq {
|
||||||
pool: string
|
pool: string
|
||||||
|
strategy?: string
|
||||||
start_date: string
|
start_date: string
|
||||||
end_date: string
|
end_date: string
|
||||||
initial_cash: number
|
initial_cash: number
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<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 { ElMessage } from 'element-plus'
|
||||||
import type { EChartsCoreOption } from 'echarts'
|
import type { EChartsCoreOption } from 'echarts'
|
||||||
import { useChart } from '@/composables/useChart'
|
import { useChart } from '@/composables/useChart'
|
||||||
@@ -28,6 +28,7 @@ let pollTimer: ReturnType<typeof setInterval> | null = null
|
|||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
pool: 'hs300_subset',
|
pool: 'hs300_subset',
|
||||||
|
strategy: 'all_weather',
|
||||||
start: '2024-01-01',
|
start: '2024-01-01',
|
||||||
end: '2024-02-29',
|
end: '2024-02-29',
|
||||||
cash: 1_000_000,
|
cash: 1_000_000,
|
||||||
@@ -39,6 +40,17 @@ const poolOptions = [
|
|||||||
{ label: '全市场(慢,非 MVP)', value: 'all' },
|
{ 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 equityEl = ref<HTMLDivElement>()
|
||||||
const { setOption: setEquityOption } = useChart(equityEl)
|
const { setOption: setEquityOption } = useChart(equityEl)
|
||||||
@@ -95,6 +107,7 @@ async function onSubmit(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const tid = await postPortfolioBacktest({
|
const tid = await postPortfolioBacktest({
|
||||||
pool: form.pool,
|
pool: form.pool,
|
||||||
|
strategy: form.strategy,
|
||||||
start_date: form.start,
|
start_date: form.start,
|
||||||
end_date: form.end,
|
end_date: form.end,
|
||||||
initial_cash: form.cash,
|
initial_cash: form.cash,
|
||||||
@@ -150,7 +163,7 @@ async function onSubmit(): Promise<void> {
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="page-title">组合策略回测</h2>
|
<h2 class="page-title">组合策略回测</h2>
|
||||||
<p class="page-subtitle">
|
<p class="page-subtitle">
|
||||||
BulletTrade + 全天候轮动策略 · 本地执行回测 · MVP 验证链路
|
BulletTrade + {{ strategyLabel }} · 本地执行回测 · MVP 验证链路
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,6 +182,17 @@ async function onSubmit(): Promise<void> {
|
|||||||
</el-select>
|
</el-select>
|
||||||
<span class="muted form-hint">MVP 默认 HS300 子集(20-30 只),快速验证链路</span>
|
<span class="muted form-hint">MVP 默认 HS300 子集(20-30 只),快速验证链路</span>
|
||||||
</el-form-item>
|
</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-form-item label="开始日期">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="form.start"
|
v-model="form.start"
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ class PortfolioBacktestRequest(BaseModel):
|
|||||||
end_date: str = Field(default="2024-02-29", description="YYYY-MM-DD")
|
end_date: str = Field(default="2024-02-29", description="YYYY-MM-DD")
|
||||||
initial_cash: float = Field(default=1_000_000.0, description="初始资金(元)")
|
initial_cash: float = Field(default=1_000_000.0, description="初始资金(元)")
|
||||||
benchmark: str = Field(default="000300.XSHG", 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)])
|
@router.post("/portfolio/backtest", dependencies=[Depends(verify_token)])
|
||||||
@@ -43,6 +47,7 @@ async def run_portfolio_backtest(req: PortfolioBacktestRequest):
|
|||||||
end=req.end_date,
|
end=req.end_date,
|
||||||
cash=req.initial_cash,
|
cash=req.initial_cash,
|
||||||
benchmark=req.benchmark,
|
benchmark=req.benchmark,
|
||||||
|
strategy=req.strategy,
|
||||||
max_pool=30,
|
max_pool=30,
|
||||||
provider_config=None,
|
provider_config=None,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -47,12 +47,13 @@ def run_portfolio_task(spec: dict) -> Any:
|
|||||||
end = spec["end"]
|
end = spec["end"]
|
||||||
cash = spec["cash"]
|
cash = spec["cash"]
|
||||||
benchmark = spec["benchmark"]
|
benchmark = spec["benchmark"]
|
||||||
|
strategy = spec.get("strategy", "all_weather")
|
||||||
max_pool = spec["max_pool"]
|
max_pool = spec["max_pool"]
|
||||||
provider_config = spec.get("provider_config")
|
provider_config = spec.get("provider_config")
|
||||||
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)
|
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:]))
|
logger.info("[portfolio_worker] task=%s running: %s", task_id, " ".join(argv[3:]))
|
||||||
|
|
||||||
proc = subprocess.run(
|
proc = subprocess.run(
|
||||||
@@ -108,6 +109,7 @@ def run_portfolio_task(spec: dict) -> Any:
|
|||||||
def _build_argv(
|
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",
|
||||||
) -> tuple[list[str], Optional[str]]:
|
) -> tuple[list[str], Optional[str]]:
|
||||||
"""Three-machine adaptive argv construction.
|
"""Three-machine adaptive argv construction.
|
||||||
|
|
||||||
@@ -122,6 +124,7 @@ def _build_argv(
|
|||||||
sys.executable, "-X", "utf8", "-m", "sanguo_portfolio.runner_backtest",
|
sys.executable, "-X", "utf8", "-m", "sanguo_portfolio.runner_backtest",
|
||||||
"--json", "--start", start, "--end", end,
|
"--json", "--start", start, "--end", end,
|
||||||
"--cash", str(cash), "--benchmark", benchmark,
|
"--cash", str(cash), "--benchmark", benchmark,
|
||||||
|
"--strategy", strategy,
|
||||||
"--max-pool", str(max_pool),
|
"--max-pool", str(max_pool),
|
||||||
]
|
]
|
||||||
# NAS container: unified provider reads NAS authoritative data layer
|
# 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"{_VPS_PYTHON} -X utf8 -m sanguo_portfolio.runner_backtest --json "
|
||||||
f"--start {start} --end {end} "
|
f"--start {start} --end {end} "
|
||||||
f"--cash {cash} --benchmark {benchmark} "
|
f"--cash {cash} --benchmark {benchmark} "
|
||||||
|
f"--strategy {strategy} "
|
||||||
f"--max-pool {max_pool}"
|
f"--max-pool {max_pool}"
|
||||||
)
|
)
|
||||||
ssh_argv = [
|
ssh_argv = [
|
||||||
|
|||||||
@@ -122,7 +122,8 @@ class Orchestrator:
|
|||||||
return task_id
|
return task_id
|
||||||
|
|
||||||
async def submit_portfolio(self, start: str, end: str, cash: float,
|
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:
|
provider_config=None) -> str:
|
||||||
"""Submit a portfolio backtest task asynchronously.
|
"""Submit a portfolio backtest task asynchronously.
|
||||||
|
|
||||||
@@ -139,6 +140,7 @@ class Orchestrator:
|
|||||||
end=end,
|
end=end,
|
||||||
cash=cash,
|
cash=cash,
|
||||||
benchmark=benchmark,
|
benchmark=benchmark,
|
||||||
|
strategy=strategy,
|
||||||
max_pool=max_pool,
|
max_pool=max_pool,
|
||||||
provider_config=provider_config,
|
provider_config=provider_config,
|
||||||
db_path=self.db_path,
|
db_path=self.db_path,
|
||||||
|
|||||||
Reference in New Issue
Block a user