refactor portfolio settings and candidate pool handling with fallback
This commit is contained in:
parent
c57fb7edd1
commit
1eaf0fd69a
@ -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(
|
||||||
"选择博弈框架",
|
"选择博弈框架",
|
||||||
|
|||||||
@ -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}")
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user