From 1eaf0fd69ae7177d23a7ecee40172de40501476b Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 11 Oct 2025 21:12:10 +0800 Subject: [PATCH] refactor portfolio settings and candidate pool handling with fallback --- app/ui/views/backtest.py | 29 ++++++++++++++++++++++++----- app/ui/views/today.py | 29 +++++++++++++++++++++++++---- app/utils/portfolio.py | 24 ++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/app/ui/views/backtest.py b/app/ui/views/backtest.py index da10714..420afec 100644 --- a/app/ui/views/backtest.py +++ b/app/ui/views/backtest.py @@ -26,7 +26,10 @@ from app.llm.templates import TemplateRegistry from app.utils import alerts from app.utils.config import get_config, save_config 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 @@ -43,6 +46,7 @@ def render_backtest_review() -> None: st.header("回测与复盘") st.caption("1. 基于历史数据复盘当前策略;2. 借助强化学习/调参探索更优参数组合。") app_cfg = get_config() + portfolio_snapshot = get_portfolio_settings_snapshot() default_start, default_end = default_backtest_range(window_days=60) LOGGER.debug( "回测默认参数: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") end_date = col2.date_input("结束日期", value=default_end, key="bt_end_date") - latest_candidates = list_investment_pool(limit=50) - candidate_codes = [item.ts_code for item in latest_candidates] + candidate_records, candidate_fallback = get_candidate_pool(limit=50) + candidate_codes = [item.ts_code for item in candidate_records] default_universe = ",".join(candidate_codes) if candidate_codes else "000001.SZ" universe_text = st.text_input( "股票列表(逗号分隔)", @@ -71,25 +75,40 @@ def render_backtest_review() -> None: help="默认载入最新候选池,如需自定义可直接编辑。", ) 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) 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") 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( "组合初始资金", - value=float(app_cfg.portfolio.initial_capital), + value=initial_capital_default, step=100000.0, format="%.0f", key="bt_initial_capital", ) initial_capital = max(0.0, float(initial_capital)) + position_limits = portfolio_snapshot.get("position_limits", {}) backtest_params = { "target": float(target), "stop": float(stop), "hold_days": int(hold_days), "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] selected_structure_values = st.multiselect( "选择博弈框架", diff --git a/app/ui/views/today.py b/app/ui/views/today.py index f5f00dc..0454acb 100644 --- a/app/ui/views/today.py +++ b/app/ui/views/today.py @@ -11,7 +11,11 @@ import pandas as pd import streamlit as st 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.ui.shared import ( @@ -132,6 +136,17 @@ def render_today_plan() -> None: st.caption(f"最新交易日:{latest_trade_date.isoformat()}(统计数据请见左侧系统监控)") else: 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: with db_session(read_only=True) as conn: date_rows = conn.execute( @@ -175,17 +190,22 @@ def render_today_plan() -> None: ).fetchall() 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: - st.caption( + message = ( f"候选池包含 {len(candidate_records)} 个标的:" + "、".join(item.ts_code for item in candidate_records[:12]) + ("…" if len(candidate_records) > 12 else "") ) + if fallback_used: + message += "(使用最新候选池)" + st.caption(message) if candidate_records: candidate_codes = [item.ts_code for item in candidate_records] symbols = list(dict.fromkeys(candidate_codes + symbols)) + else: + st.caption("所选日期暂无候选池数据,仍可查看代理决策记录。") detail_tab, assistant_tab = st.tabs(["标的详情", "投资助理模式"]) 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_info = candidate_map.get(ts_code) 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[1].metric("状态", candidate_info.status or "-") info_cols[2].metric("更新时间", candidate_info.created_at or "-") + info_cols[3].metric("行业", candidate_info.industry or "-") if candidate_info.rationale: st.caption(f"候选理由:{candidate_info.rationale}") diff --git a/app/utils/portfolio.py b/app/utils/portfolio.py index 021b31a..77993b4 100644 --- a/app/utils/portfolio.py +++ b/app/utils/portfolio.py @@ -120,6 +120,30 @@ def list_investment_pool( 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 class PortfolioPosition: id: int