refactor portfolio settings and candidate pool handling with fallback

This commit is contained in:
Your Name 2025-10-11 21:12:10 +08:00
parent c57fb7edd1
commit 1eaf0fd69a
3 changed files with 73 additions and 9 deletions

View File

@ -26,7 +26,10 @@ from app.llm.templates import TemplateRegistry
from app.utils import alerts from app.utils import alerts
from app.utils.config import get_config, save_config from app.utils.config import get_config, save_config
from app.utils.tuning import log_tuning_result from app.utils.tuning import log_tuning_result
from app.utils.portfolio import list_investment_pool from app.utils.portfolio import (
get_candidate_pool,
get_portfolio_settings_snapshot,
)
from app.utils.db import db_session from app.utils.db import db_session
@ -43,6 +46,7 @@ def render_backtest_review() -> None:
st.header("回测与复盘") st.header("回测与复盘")
st.caption("1. 基于历史数据复盘当前策略2. 借助强化学习/调参探索更优参数组合。") st.caption("1. 基于历史数据复盘当前策略2. 借助强化学习/调参探索更优参数组合。")
app_cfg = get_config() app_cfg = get_config()
portfolio_snapshot = get_portfolio_settings_snapshot()
default_start, default_end = default_backtest_range(window_days=60) default_start, default_end = default_backtest_range(window_days=60)
LOGGER.debug( LOGGER.debug(
"回测默认参数start=%s end=%s universe=%s target=%s stop=%s hold_days=%s initial_capital=%s", "回测默认参数start=%s end=%s universe=%s target=%s stop=%s hold_days=%s initial_capital=%s",
@ -61,8 +65,8 @@ def render_backtest_review() -> None:
start_date = col1.date_input("开始日期", value=default_start, key="bt_start_date") start_date = col1.date_input("开始日期", value=default_start, key="bt_start_date")
end_date = col2.date_input("结束日期", value=default_end, key="bt_end_date") end_date = col2.date_input("结束日期", value=default_end, key="bt_end_date")
latest_candidates = list_investment_pool(limit=50) candidate_records, candidate_fallback = get_candidate_pool(limit=50)
candidate_codes = [item.ts_code for item in latest_candidates] candidate_codes = [item.ts_code for item in candidate_records]
default_universe = ",".join(candidate_codes) if candidate_codes else "000001.SZ" default_universe = ",".join(candidate_codes) if candidate_codes else "000001.SZ"
universe_text = st.text_input( universe_text = st.text_input(
"股票列表(逗号分隔)", "股票列表(逗号分隔)",
@ -71,25 +75,40 @@ def render_backtest_review() -> None:
help="默认载入最新候选池,如需自定义可直接编辑。", help="默认载入最新候选池,如需自定义可直接编辑。",
) )
if candidate_codes: if candidate_codes:
st.caption(f"候选池载入 {len(candidate_codes)} 个标的:{''.join(candidate_codes[:10])}{'' if len(candidate_codes)>10 else ''}") message = f"候选池载入 {len(candidate_codes)} 个标的:{''.join(candidate_codes[:10])}{'' if len(candidate_codes)>10 else ''}"
if candidate_fallback:
message += "(使用最新候选池作为回退)"
st.caption(message)
col_target, col_stop, col_hold, col_cap = st.columns(4) col_target, col_stop, col_hold, col_cap = st.columns(4)
target = col_target.number_input("目标收益0.035 表示 3.5%", value=0.035, step=0.005, format="%.3f", key="bt_target") target = col_target.number_input("目标收益0.035 表示 3.5%", value=0.035, step=0.005, format="%.3f", key="bt_target")
stop = col_stop.number_input("止损收益(例:-0.015 表示 -1.5%", value=-0.015, step=0.005, format="%.3f", key="bt_stop") stop = col_stop.number_input("止损收益(例:-0.015 表示 -1.5%", value=-0.015, step=0.005, format="%.3f", key="bt_stop")
hold_days = col_hold.number_input("持有期(交易日)", value=10, step=1, key="bt_hold_days") hold_days = col_hold.number_input("持有期(交易日)", value=10, step=1, key="bt_hold_days")
initial_capital_default = float(portfolio_snapshot["initial_capital"])
initial_capital = col_cap.number_input( initial_capital = col_cap.number_input(
"组合初始资金", "组合初始资金",
value=float(app_cfg.portfolio.initial_capital), value=initial_capital_default,
step=100000.0, step=100000.0,
format="%.0f", format="%.0f",
key="bt_initial_capital", key="bt_initial_capital",
) )
initial_capital = max(0.0, float(initial_capital)) initial_capital = max(0.0, float(initial_capital))
position_limits = portfolio_snapshot.get("position_limits", {})
backtest_params = { backtest_params = {
"target": float(target), "target": float(target),
"stop": float(stop), "stop": float(stop),
"hold_days": int(hold_days), "hold_days": int(hold_days),
"initial_capital": initial_capital, "initial_capital": initial_capital,
"max_position_weight": float(position_limits.get("max_position", 0.2)),
"max_total_positions": int(position_limits.get("max_total_positions", 20)),
} }
st.caption(
"组合约束:单仓上限 {max_pos:.0%} 最大持仓 {max_count} 行业敞口 {sector:.0%}".format(
max_pos=backtest_params["max_position_weight"],
max_count=position_limits.get("max_total_positions", 20),
sector=position_limits.get("max_sector_exposure", 0.35),
)
)
structure_options = [item.value for item in GameStructure] structure_options = [item.value for item in GameStructure]
selected_structure_values = st.multiselect( selected_structure_values = st.multiselect(
"选择博弈框架", "选择博弈框架",

View File

@ -11,7 +11,11 @@ import pandas as pd
import streamlit as st import streamlit as st
from app.backtest.engine import BacktestEngine, PortfolioState, BtConfig from app.backtest.engine import BacktestEngine, PortfolioState, BtConfig
from app.utils.portfolio import InvestmentCandidate, list_investment_pool from app.utils.portfolio import (
InvestmentCandidate,
get_candidate_pool,
get_portfolio_settings_snapshot,
)
from app.utils.db import db_session from app.utils.db import db_session
from app.ui.shared import ( from app.ui.shared import (
@ -132,6 +136,17 @@ def render_today_plan() -> None:
st.caption(f"最新交易日:{latest_trade_date.isoformat()}(统计数据请见左侧系统监控)") st.caption(f"最新交易日:{latest_trade_date.isoformat()}(统计数据请见左侧系统监控)")
else: else:
st.caption("统计与决策概览现已移至左侧'系统监控'侧栏。") st.caption("统计与决策概览现已移至左侧'系统监控'侧栏。")
portfolio_snapshot = get_portfolio_settings_snapshot()
limits = portfolio_snapshot.get("position_limits", {})
st.caption(
"组合约束:单仓上限 {max_pos:.0%} 最小仓位 {min_pos:.0%} 最大持仓数 {max_cnt} 行业上限 {sector:.0%}".format(
max_pos=limits.get("max_position", 0.2),
min_pos=limits.get("min_position", 0.02),
max_cnt=int(limits.get("max_total_positions", 20)),
sector=limits.get("max_sector_exposure", 0.35),
)
)
try: try:
with db_session(read_only=True) as conn: with db_session(read_only=True) as conn:
date_rows = conn.execute( date_rows = conn.execute(
@ -175,17 +190,22 @@ def render_today_plan() -> None:
).fetchall() ).fetchall()
symbols = [row["ts_code"] for row in code_rows] symbols = [row["ts_code"] for row in code_rows]
candidate_records = list_investment_pool(trade_date=trade_date) candidate_records, fallback_used = get_candidate_pool(trade_date=trade_date)
if candidate_records: if candidate_records:
st.caption( message = (
f"候选池包含 {len(candidate_records)} 个标的:" f"候选池包含 {len(candidate_records)} 个标的:"
+ "".join(item.ts_code for item in candidate_records[:12]) + "".join(item.ts_code for item in candidate_records[:12])
+ ("" if len(candidate_records) > 12 else "") + ("" if len(candidate_records) > 12 else "")
) )
if fallback_used:
message += "(使用最新候选池)"
st.caption(message)
if candidate_records: if candidate_records:
candidate_codes = [item.ts_code for item in candidate_records] candidate_codes = [item.ts_code for item in candidate_records]
symbols = list(dict.fromkeys(candidate_codes + symbols)) symbols = list(dict.fromkeys(candidate_codes + symbols))
else:
st.caption("所选日期暂无候选池数据,仍可查看代理决策记录。")
detail_tab, assistant_tab = st.tabs(["标的详情", "投资助理模式"]) detail_tab, assistant_tab = st.tabs(["标的详情", "投资助理模式"])
with assistant_tab: with assistant_tab:
@ -339,10 +359,11 @@ def _render_today_plan_symbol_view(
candidate_map = {item.ts_code: item for item in candidate_records} candidate_map = {item.ts_code: item for item in candidate_records}
candidate_info = candidate_map.get(ts_code) candidate_info = candidate_map.get(ts_code)
if candidate_info: if candidate_info:
info_cols = st.columns(3) info_cols = st.columns(4)
info_cols[0].metric("候选评分", f"{(candidate_info.score or 0):.3f}") info_cols[0].metric("候选评分", f"{(candidate_info.score or 0):.3f}")
info_cols[1].metric("状态", candidate_info.status or "-") info_cols[1].metric("状态", candidate_info.status or "-")
info_cols[2].metric("更新时间", candidate_info.created_at or "-") info_cols[2].metric("更新时间", candidate_info.created_at or "-")
info_cols[3].metric("行业", candidate_info.industry or "-")
if candidate_info.rationale: if candidate_info.rationale:
st.caption(f"候选理由:{candidate_info.rationale}") st.caption(f"候选理由:{candidate_info.rationale}")

View File

@ -120,6 +120,30 @@ def list_investment_pool(
return candidates return candidates
def get_candidate_pool(
*,
trade_date: Optional[str] = None,
status: Optional[Iterable[str]] = None,
limit: int = 200,
) -> tuple[List[InvestmentCandidate], bool]:
"""Return candidate pool records with optional fallback to latest date.
Returns:
(candidates, fallback_used)
"""
candidates = list_investment_pool(trade_date=trade_date, status=status, limit=limit)
if candidates or trade_date is None:
return candidates, False
latest_candidates = list_investment_pool(status=status, limit=limit)
return latest_candidates, bool(latest_candidates)
def get_portfolio_settings_snapshot() -> Dict[str, Any]:
"""Return current portfolio settings as a dict for UI consumption."""
return get_portfolio_config()
@dataclass @dataclass
class PortfolioPosition: class PortfolioPosition:
id: int id: int