diff --git a/.gitignore b/.gitignore index a2cc304..9eb1cd5 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ app/data/*.json # Streamlit temporary files .streamlit/ +# Local references +Refer-TradingAgents-CN + # System files .DS_Store Thumbs.db diff --git a/app/backtest/engine.py b/app/backtest/engine.py index 41cf856..0a21ec8 100644 --- a/app/backtest/engine.py +++ b/app/backtest/engine.py @@ -259,8 +259,6 @@ class BacktestEngine: decision_callback(ts_code, trade_date, context, decision) except Exception: # noqa: BLE001 LOGGER.exception("决策回调执行失败", extra=LOG_EXTRA) - # TODO: translate decisions into fills, holdings, and NAV updates. - _ = state return records def record_agent_state(self, context: AgentContext, decision: Decision) -> None: @@ -700,12 +698,156 @@ def run_backtest( ) -> BacktestResult: engine = BacktestEngine(cfg) result = engine.run(decision_callback=decision_callback) - with db_session() as conn: - _ = conn - # Implementation should persist bt_nav, bt_trades, and bt_report rows. + _persist_backtest_results(cfg, result) return result +def _persist_backtest_results(cfg: BtConfig, result: BacktestResult) -> None: + """Persist backtest configuration, NAV path, trades and summary metrics.""" + + nav_rows: List[tuple] = [] + trade_rows: List[tuple] = [] + summary_payload: Dict[str, object] = {} + + if result.nav_series: + first_nav = float(result.nav_series[0].get("nav", 0.0) or 0.0) + peak_nav = first_nav + prev_nav: Optional[float] = None + max_drawdown = 0.0 + for entry in result.nav_series: + trade_date = str(entry.get("trade_date", "")) + nav_val = float(entry.get("nav", 0.0) or 0.0) + cash = float(entry.get("cash", 0.0) or 0.0) + market_value = float(entry.get("market_value", 0.0) or 0.0) + realized = float(entry.get("realized_pnl", 0.0) or 0.0) + unrealized = float(entry.get("unrealized_pnl", 0.0) or 0.0) + + if nav_val > peak_nav: + peak_nav = nav_val + drawdown = (peak_nav - nav_val) / peak_nav if peak_nav else 0.0 + max_drawdown = max(max_drawdown, drawdown) + + if prev_nav is None or prev_nav == 0.0: + ret_val = 0.0 + else: + ret_val = (nav_val / prev_nav) - 1.0 + prev_nav = nav_val + + info_payload = { + "cash": cash, + "market_value": market_value, + "realized_pnl": realized, + "unrealized_pnl": unrealized, + } + nav_rows.append( + ( + cfg.id, + trade_date, + nav_val, + float(ret_val), + None, + None, + float(drawdown), + json.dumps(info_payload, ensure_ascii=False), + ) + ) + + last_nav = float(result.nav_series[-1].get("nav", 0.0) or 0.0) + total_return = (last_nav / first_nav - 1.0) if first_nav else 0.0 + summary_payload.update( + { + "start_nav": first_nav, + "end_nav": last_nav, + "total_return": total_return, + "max_drawdown": max_drawdown, + "days": len(result.nav_series), + } + ) + + if result.trades: + for trade in result.trades: + trade_date = str(trade.get("trade_date", "")) + ts_code = str(trade.get("ts_code", "")) + side = str(trade.get("action", "")).lower() + price = float(trade.get("price", 0.0) or 0.0) + qty = float(trade.get("quantity", 0.0) or 0.0) + reason_payload = { + "confidence": trade.get("confidence"), + "target_weight": trade.get("target_weight"), + "value": trade.get("value"), + } + trade_rows.append( + ( + cfg.id, + ts_code, + trade_date, + side, + price, + qty, + json.dumps(reason_payload, ensure_ascii=False), + ) + ) + summary_payload["trade_count"] = len(trade_rows) + + cfg_payload = { + "id": cfg.id, + "name": cfg.name, + "start_date": cfg.start_date.isoformat(), + "end_date": cfg.end_date.isoformat(), + "universe": cfg.universe, + "params": cfg.params, + "method": cfg.method, + } + + with db_session() as conn: + conn.execute( + """ + INSERT OR REPLACE INTO bt_config (id, name, start_date, end_date, universe, params) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + cfg.id, + cfg.name, + cfg.start_date.isoformat(), + cfg.end_date.isoformat(), + ",".join(cfg.universe), + json.dumps(cfg.params, ensure_ascii=False), + ), + ) + + conn.execute("DELETE FROM bt_nav WHERE cfg_id = ?", (cfg.id,)) + conn.execute("DELETE FROM bt_trades WHERE cfg_id = ?", (cfg.id,)) + conn.execute("DELETE FROM bt_report WHERE cfg_id = ?", (cfg.id,)) + + if nav_rows: + conn.executemany( + """ + INSERT INTO bt_nav (cfg_id, trade_date, nav, ret, pos_count, turnover, dd, info) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + nav_rows, + ) + + if trade_rows: + conn.executemany( + """ + INSERT INTO bt_trades (cfg_id, ts_code, trade_date, side, price, qty, reason) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + trade_rows, + ) + + summary_payload.setdefault("universe", cfg.universe) + summary_payload.setdefault("method", cfg.method) + conn.execute( + """ + INSERT OR REPLACE INTO bt_report (cfg_id, summary) + VALUES (?, ?) + """, + (cfg.id, json.dumps(summary_payload, ensure_ascii=False, default=str)), + ) + + def _candidate_status(action: AgentAction, requires_review: bool) -> str: mapping = { AgentAction.SELL: "exit", diff --git a/docs/TODO.md b/docs/TODO.md new file mode 100644 index 0000000..6b81d02 --- /dev/null +++ b/docs/TODO.md @@ -0,0 +1,35 @@ +# 项目待办清单 + +> 用于跟踪现阶段尚未完成或需要后续完善的工作,便于规划优先级。 + +## 1. UI 与日志增强 +- 今日计划页增加“一键重评估”入口,以及日志钻取 / 历史对比视图(对齐 README 中的架构目标)。 +- 回测页面支持多版本实验管理(对比不同提示/温度的收益曲线),与 `tuning_results` 记录联动。 +- Streamlit 聚焦监控场景,补充实时指标面板、异常日志钻取与“仅监控不干预”模式的一键复评策略。 + +## 2. 数据与特征层 +- 实现 `app/features/factors.py` 中的 `compute_factors()`,补齐因子计算与持久化流程。 +- 完成 `app/ingest/rss.py` 的 RSS 拉取与写库逻辑,打通新闻与情绪数据源。 +- 强化 `DataBroker` 的取数校验、缓存与回退策略,确保行情/特征补数统一自动化,减少人工兜底。 +- 围绕动量、估值、流动性等核心信号扩展轻量高质量因子集,全部由程序生成,满足端到端自动决策需求。 + +## 3. 决策优化与强化学习 +- 扩展 `DecisionEnv` 的动作空间(提示版本、部门温度、function 调用策略等),不仅限于代理权重调节。 +- 引入 Bandit / 贝叶斯优化或 RL 算法探索动作空间,并将 `portfolio_snapshots`、`portfolio_trades` 指标纳入奖励约束。 +- 构建实时持仓/成交数据写入链路,使线上监控与离线调参共用同一数据源。 +- 借鉴 TradingAgents-CN 的做法:拆分环境与策略、提供训练脚本/配置,并输出丰富的评估指标(如 Sharpe、Sortino、基准对比)。 +- 完善 `BacktestEngine` 的成交撮合、风险阈值与指标输出,让回测信号直接对接执行端,形成无人值守的自动闭环。 + +## 4. 测试与验证 +- 补充部门上下文构造、多模型调用、回测指标生成等核心路径的单元 / 集成测试。 +- 建立决策流程的回归测试用例,确保提示模板或配置调整后行为可复现。 +- 编写示例 Notebook / end-to-end 教程,参照 TradingAgents-CN 的教学方式,覆盖“数据→回测→调参→评估”全流程。 +- 针对数据摄取、策略主干与回测指标建立自动化验证管线,作为无人干预运行的质量护栏。 + +## 5. 文档同步 +- 随功能推进,更新 README 与讨论文档,确保描述与实际实现保持一致。 + +## 6. LLM 协同与配置 +- 精简 Provider 列表、强化 function-calling 架构,完善降级和重试策略,并用配置化的角色提示与数据 Scope 提高模型行为可控性。 + +(最后更新:2025-09-29) diff --git a/docs/decision_optimization_notes.md b/docs/decision_optimization_notes.md index 84b54f4..32a8407 100644 --- a/docs/decision_optimization_notes.md +++ b/docs/decision_optimization_notes.md @@ -33,7 +33,7 @@ ## 已完成的日志改进 - `agent_utils` 表新增 `_telemetry` 与 `_department_telemetry` JSON 字段(存于 `utils` 列内部),记录每个部门的 provider、模型、温度、回合数、工具调用列表与 token 统计,可在 Streamlit “部门意见”详情页展开查看。 - `app/data/logs/agent_*.log` 会追加 `telemetry` 行,保存每轮函数调用的摘要,方便离线分析提示版本与 LLM 配置对决策的影响。 -- Streamlit 侧边栏监听 `llm.metrics` 的实时事件,并以 ~0.75 秒节流频率刷新“系统监控”,既保证日志到达后快速更新,也避免刷屏造成 UI 闪烁。 +- Streamlit 侧边栏监听 `llm.metrics` 的实时事件,并使用原位组件刷新“系统监控”,避免增量追加或节流导致的失效,同时确保实时数据推送稳定。 - 新增投资管理数据层:SQLite 中创建 `investment_pool`、`portfolio_positions`、`portfolio_trades`、`portfolio_snapshots` 四张表;`app/utils/portfolio.py` 提供访问接口,今日计划页可实时展示候选池、持仓与成交。 - 回测引擎 `record_agent_state()` 现同步写入 `investment_pool`,将每日全局决策的置信度、部门标签与目标权重落库,作为后续提示参数调优与候选池管理的基础数据。 - `app/backtest/decision_env.py` 引入 `DecisionEnv`,用单步 RL/Gym 风格接口封装回测:动作 → 权重映射 → 回测 → 奖励(收益 - 0.5×回撤),同时输出 NAV、交易与行动权重,方便与 Bandit/PPO 等算法对接。