diff --git a/deploy/profile-scripts/price_monitor.py b/deploy/profile-scripts/price_monitor.py index 9d3ea89c..e72ec2c3 100644 --- a/deploy/profile-scripts/price_monitor.py +++ b/deploy/profile-scripts/price_monitor.py @@ -773,6 +773,15 @@ def run_once(round_label=""): # 取消超时定时器(正常完成) signal.alarm(0) + # ── 策略追踪评估(2026-07-27 老爸:检查推荐是否触发止盈/止损)── + try: + import mofin_db + tracked = mofin_db.check_strategy_outcomes(conn) + if tracked: + print(f" [TRACK] {tracked}条推荐触发止盈/止损", flush=True) + except Exception as e: + print(f" [TRACK] 检查失败: {e}", flush=True) + # 清理进程锁 try: os.remove("/tmp/price_monitor.lock") diff --git a/mofin_db.py b/mofin_db.py index d97a1556..ef57ce11 100644 --- a/mofin_db.py +++ b/mofin_db.py @@ -285,6 +285,46 @@ def init_all_tables(conn: sqlite3.Connection): ); CREATE INDEX IF NOT EXISTS idx_strategy_history_code ON strategy_history(code, snapshotted_at); + -- 策略追踪评估(2026-07-27 老爸:每条推荐操作的完整生命周期跟踪) + -- 每个版本一条记录,策略变更时自动追加新版本 + CREATE TABLE IF NOT EXISTS strategy_tracking ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code TEXT NOT NULL, + name TEXT, + version_seq INTEGER DEFAULT 1, -- 该股票的第几个策略版本 + -- 推荐时的快照 + tracked_at TEXT NOT NULL DEFAULT (datetime('now','localtime')), + timing_signal TEXT, + rec_score INTEGER DEFAULT 0, + rr_ratio REAL, + entry_low REAL, + entry_high REAL, + entry_mid REAL, + stop_loss REAL, + take_profit REAL, + position_advice TEXT, + price_at_track REAL, -- 记录时的市价 + -- 区别于前一版本的变化摘要 + change_summary TEXT, + -- 结果跟踪 + status TEXT DEFAULT 'active' CHECK(status IN ('active','hit_tp','hit_sl','expired','manual_close')), + closed_at TEXT, + close_price REAL, + close_reason TEXT, + theoretical_pnl REAL, -- 理论盈亏%(基于中值买入价) + -- 实操数据(由用户或导入脚本填入) + actual_action TEXT, -- "买入600股@148.86" + actual_entry REAL, + actual_shares INTEGER, + actual_exit REAL, + actual_pnl REAL, + actual_exit_reason TEXT, + notes TEXT + ); + CREATE INDEX IF NOT EXISTS idx_track_code ON strategy_tracking(code); + CREATE INDEX IF NOT EXISTS idx_track_status ON strategy_tracking(status); + CREATE INDEX IF NOT EXISTS idx_track_date ON strategy_tracking(tracked_at); + -- 自选股 CREATE TABLE IF NOT EXISTS watchlist_stocks ( code TEXT PRIMARY KEY REFERENCES stocks(code), @@ -1373,13 +1413,14 @@ def sync_recommend_tag(conn, code: str, timing_signal: str): _pct = max(5, min(20, _pct)) _pos_auto = f"{int(_pct)}%(系统按RR{_rr_pos:.1f}自动计算,见原建议仓位)" # ── 股数换算(2026-07-27 老爸:方便快速操作)── + # 2026-07-27 修正:按总资产算仓位,不用现金(现金波动剧烈) try: _lp = conn.execute("SELECT price FROM live_prices WHERE code=?", (code,)).fetchone() - _cash = conn.execute("SELECT cash FROM portfolio_summary WHERE id=1").fetchone() - if _lp and _lp[0] and _cash and _cash[0]: + _ta = conn.execute("SELECT total_assets FROM portfolio_summary WHERE id=1").fetchone() + if _lp and _lp[0] and _ta and _ta[0]: _price = float(_lp[0]) - _cash_val = float(_cash[0]) - _shares_raw = _cash_val * _pct / 100.0 / _price + _total = float(_ta[0]) + _shares_raw = _total * _pct / 100.0 / _price if _shares_raw >= 100: _shares = int(_shares_raw / 100) * 100 # A股整手 else: @@ -1426,6 +1467,8 @@ def sync_recommend_tag(conn, code: str, timing_signal: str): "WHERE code=? AND status='active' AND tag='current_recommend'", (code,)) conn.commit() + # ── 策略版本追踪(2026-07-27 老爸:每次tag变更都记录到评估表)── + track_strategy_version(conn, code) except Exception as e: print(f" [TAG SYNC] {code} 失败: {e}", flush=True) @@ -1495,6 +1538,117 @@ def enqueue_recommend(conn, code: str): return False +def track_strategy_version(conn, code: str): + """版本化策略追踪:每次策略变更(sync_recommend_tag 后)自动记录新版本。 + 只在 tag='current_recommend' 时记录;与上一条相比有实质变更才追加新版本。""" + try: + row = conn.execute( + "SELECT name, timing_signal, rec_score, rr_ratio, entry_low, entry_high, " + "stop_loss, take_profit, position_advice, tag FROM holding_strategies " + "WHERE code=? AND status='active'", (code,)).fetchone() + if not row: + return + name, sig, score, rr, el, eh, sl, tp, pos, tag = row + if tag != 'current_recommend': + return # 非推荐状态不追踪 + + lp = conn.execute("SELECT price FROM live_prices WHERE code=?", (code,)).fetchone() + price = lp[0] if lp and lp[0] else 0 + mid = round((el + eh) / 2, 2) if el > 0 and eh > el else 0 + + # 查上一个版本 + prev = conn.execute( + "SELECT entry_low, entry_high, stop_loss, take_profit, rr_ratio, rec_score, " + "status FROM strategy_tracking WHERE code=? ORDER BY id DESC LIMIT 1", + (code,)).fetchone() + + # 计算版本号 + last_ver = conn.execute( + "SELECT MAX(version_seq) FROM strategy_tracking WHERE code=?", (code,)).fetchone() + ver = (last_ver[0] or 0) + 1 if last_ver else 1 + + if prev and prev[6] == 'active': # 上一版本还在进行中 + if (abs((prev[0] or 0) - (el or 0)) < 0.01 and + abs((prev[1] or 0) - (eh or 0)) < 0.01 and + abs((prev[2] or 0) - (sl or 0)) < 0.01 and + abs((prev[3] or 0) - (tp or 0)) < 0.01): + # 参数没变,只更新评分和RR + conn.execute( + "UPDATE strategy_tracking SET rec_score=?, rr_ratio=?, price_at_track=?, " + "timing_signal=?, position_advice=? WHERE id=(" + "SELECT id FROM strategy_tracking WHERE code=? ORDER BY id DESC LIMIT 1)", + (score, rr, price, sig, pos, code)) + conn.commit() + return + + # 有变更 → 追加新版本 + change = "" + if prev: + parts = [] + if abs((prev[0] or 0) - (el or 0)) > 0.5: parts.append(f"区{prev[0]}→{el}") + if abs((prev[2] or 0) - (sl or 0)) > 0.5: parts.append(f"损{prev[2]}→{sl}") + if abs((prev[3] or 0) - (tp or 0)) > 0.5: parts.append(f"盈{prev[3]}→{tp}") + if abs((prev[5] or 0) - (score or 0)) >= 5: parts.append(f"评分{prev[5]}→{score}") + change = "; ".join(parts) if parts else "" + + conn.execute(""" + INSERT INTO strategy_tracking + (code, name, version_seq, tracked_at, timing_signal, rec_score, rr_ratio, + entry_low, entry_high, entry_mid, stop_loss, take_profit, + position_advice, price_at_track, change_summary) + VALUES (?,?,?,datetime('now','localtime'),?,?,?,?,?,?,?,?,?,?,?) + """, (code, name, ver, sig, score, rr, el, eh, mid, sl, tp, pos, price, change)) + conn.commit() + if change: + print(f" [TRACK] {code} v{ver}: {change}", flush=True) + except Exception as e: + print(f" [TRACK] {code} 版本记录失败: {e}", flush=True) + + +def check_strategy_outcomes(conn): + """检查所有 active 追踪版本是否触发 SL/TP,自动关闭并记录理论盈亏""" + active = conn.execute(""" + SELECT id, code, entry_mid, stop_loss, take_profit, rr_ratio + FROM strategy_tracking WHERE status='active' + """).fetchall() + + updated = 0 + for r in active: + tid, code, mid, sl, tp, rr = r + lp = conn.execute("SELECT price FROM live_prices WHERE code=?", (code,)).fetchone() + if not lp or not lp[0]: + continue + price = float(lp[0]) + mid = mid or price # fallback + + closed = False + if tp and tp > 0 and price >= tp: + pnl_pct = round((tp - mid) / mid * 100, 1) if mid > 0 else 0 + conn.execute(""" + UPDATE strategy_tracking SET status='hit_tp', closed_at=datetime('now','localtime'), + close_price=?, close_reason='止盈触发', theoretical_pnl=? + WHERE id=? + """, (price, pnl_pct, tid)) + print(f" [TRACK] {code} v{tid} 止盈! {price}≥{tp} +{pnl_pct}%", flush=True) + closed = True + elif sl and sl > 0 and price <= sl: + pnl_pct = round((sl - mid) / mid * 100, 1) if mid > 0 else -5 + conn.execute(""" + UPDATE strategy_tracking SET status='hit_sl', closed_at=datetime('now','localtime'), + close_price=?, close_reason='止损触发', theoretical_pnl=? + WHERE id=? + """, (price, pnl_pct, tid)) + print(f" [TRACK] {code} v{tid} 止损! {price}≤{sl} {pnl_pct}%", flush=True) + closed = True + + if closed: + updated += 1 + + if updated: + conn.commit() + return updated + + def flush_rec_digest(max_items=5): """把队列里的新推荐聚成一条 XMPP 摘要(RR 降序,最多 max_items 只)。 头部 1-2 只附带策略依据摘要+按现金的操盘建议。""" @@ -1967,6 +2121,9 @@ def write_holding_strategy(conn, code: str, name: str, data: dict, _existing_tag, )) conn.commit() + # ── 策略版本追踪(每次策略写入后自动记录,2026-07-27)── + if _existing_tag == 'current_recommend': + track_strategy_version(conn, code) # ── 推荐转场:LLM路径新转为 current_recommend → 记入摘要队列(不逐只推送)── if _existing_tag == 'current_recommend' and _old_tag != 'current_recommend' \ and source_trigger in ('batch_12d', 'per_stock_12d'): diff --git a/server.py b/server.py index 257f3431..f001214a 100644 --- a/server.py +++ b/server.py @@ -488,6 +488,43 @@ def api_watchlist(): return jsonify({"error": str(e)}), 500 +@app.route("/api/tracking") +def get_tracking(): + """策略追踪评估:所有推荐的历史记录和结果""" + import sqlite3 + conn = sqlite3.connect("/home/hmo/MoFin/data/mofin.db") + conn.row_factory = sqlite3.Row + rows = conn.execute(""" + SELECT st.*, lp.price as current_price + FROM strategy_tracking st + LEFT JOIN live_prices lp ON st.code = lp.code + ORDER BY + CASE st.status WHEN 'active' THEN 0 WHEN 'hit_tp' THEN 1 WHEN 'hit_sl' THEN 2 ELSE 3 END, + st.tracked_at DESC + """).fetchall() + tracks = [] + for r in rows: + d = dict(r) + # 理论盈亏:按买入区中值买入,当前价 vs 中值 + if d.get('entry_mid') and d.get('current_price') and d.get('status') == 'active': + mid = float(d['entry_mid']) + price = float(d['current_price']) + d['theoretical_pnl'] = round((price - mid) / mid * 100, 2) + tracks.append(d) + conn.close() + return jsonify({ + "tracks": tracks, + "stats": { + "total": len(tracks), + "active": sum(1 for r in tracks if r["status"] == "active"), + "hit_tp": sum(1 for r in tracks if r["status"] == "hit_tp"), + "hit_sl": sum(1 for r in tracks if r["status"] == "hit_sl"), + "expired": sum(1 for r in tracks if r["status"] == "expired"), + "manual": sum(1 for r in tracks if r["status"] == "manual_close"), + } + }) + + @app.route("/api/overview") def api_overview(): """概览数据""" @@ -1692,4 +1729,4 @@ register_routes(app) if __name__ == "__main__": port = int(os.environ.get("PORT", 8899)) print(f"🚀 MoFin Dashboard → http://0.0.0.0:{port}") - app.run(host="0.0.0.0", port=port, debug=False) \ No newline at end of file + app.run(host="0.0.0.0", port=port, debug=False) diff --git a/static/index.html b/static/index.html index 523cc849..237c5535 100644 --- a/static/index.html +++ b/static/index.html @@ -612,89 +612,64 @@ function renderMarket() { } // ── Evaluation Tab ── +let evalTimer = null; function renderEvaluation() { const el = document.getElementById('tab-evaluation'); - Promise.all([ - fetchJSON('/api/evaluation'), - fetchJSON('/api/stats/accuracy'), - ]).then(([evals, stats]) => { - const p1 = stats.phase1 || {}; - const p2 = stats.phase2 || {}; + if (evalTimer) clearInterval(evalTimer); + fetchJSON('/api/tracking').then(data => { + const tracks = data.tracks || []; + const stats = data.stats || {}; + + const statusBadge = s => { + const m = {active:'🟡进行中',hit_tp:'🟢止盈',hit_sl:'🔴止损',expired:'⚫已过期',manual_close:'🔵手动平仓'}; + return m[s] || s; + }; + el.innerHTML = `
- 策略双维度评估 ?§ - ${evals.length} 只已评估 - + 📊 策略追踪评估 + ${stats.total||0} 条记录 + 止盈 ${stats.hit_tp||0} + 止损 ${stats.hit_sl||0} + 进行中 ${stats.active||0}
-
-
-
${p1.correct||0}
-
阶段一 达标
-
-
-
${p1.wrong||0}
-
阶段一 止损
-
-
-
${p1.pending||0}
-
待验证
-
-
-
${p1.accuracy_pct||0}%
-
准确率
-
-
-
-
- - - - - - - ${evals.map(x => { - const adv = (x.advice_evaluation && x.advice_evaluation[0]) || {}; - const t = x.theoretical || {}; - const a = x.actual || {}; - const status = t.status || 'safe'; - const icon = status === 'take_profit_hit' ? '🟢' : status === 'stop_loss_hit' ? '🔴' : status === 'in_entry_zone' ? '📥' : '💤'; - return ` - - - - - - `; - }).join('')} -
股票策略理论盈亏实际盈亏状态
${x.name}
${x.code}
${x.current || (x.advice_evaluation && x.advice_evaluation[0] && x.advice_evaluation[0].current_advice || '') || '—'}${(t.theoretical_pnl_pct||0).toFixed(1)}%${(a.actual_pnl_pct||0).toFixed(1)}%${icon} ${status.replace(/_/g,' ')}
-
-
-
更新时间: ${stats.updated_at?.slice(0,16)||'—'}
- `; - // Load feedback - fetchJSON('/api/feedback').then(fb => { - const trend = fb.accuracy_trend || {}; - const completed = fb.phase1_completed_count || 0; - const reassess = fb.reassess_needed_count || 0; - if (!completed && !reassess) return; - const fbEl = document.createElement('div'); - fbEl.className = 'card p-4 mt-4'; - fbEl.innerHTML = ` -
- 🔄 反馈闭环 - 趋势: ${trend.trend||'stable'} - 完成 ${completed} - 重评 ${reassess} -
- ${(fb.feedback||[]).filter(f => f.adjustments?.length).slice(0,10).map(f => f.adjustments.map(a => ` -
- ${a.type==='phase1_success'?'✅':a.type==='phase1_failure'?'❌':'🔄'} - ${a.message} -
- `).join('')).join('')} - `; - el.appendChild(fbEl); - }); +
+ + + + + + + + + + + + + + + ${tracks.length === 0 ? '' : tracks.map(t => { + const mid = t.entry_mid ? Number(t.entry_mid).toFixed(2) : '—'; + const tPnl = t.theoretical_pnl != null ? ((t.theoretical_pnl >= 0 ? '+' : '') + t.theoretical_pnl + '%') : '—'; + const actualInfo = t.actual_action ? '' + t.actual_action + '' : (t.actual_pnl != null ? ((t.actual_pnl >= 0 ? '+' : '') + t.actual_pnl + '%') : '—'); + const rowBg = t.status === 'hit_tp' ? 'bg-green-900/10' : t.status === 'hit_sl' ? 'bg-red-900/10' : ''; + return '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + }).join('')} +
股票推荐时间评分RR买入区(中值)止损/止盈推荐价推荐仓位理论盈亏实际盈亏状态
暂无追踪记录
' + (t.name||'') + '
' + t.code + '
' + (t.tracked_at||'').slice(0,16) + '' + (t.rec_score||'—') + '' + (t.rr_ratio ? t.rr_ratio.toFixed(2) : '—') + '' + (t.entry_low||'—') + ' → ' + mid + ' ← ' + (t.entry_high||'—') + '损' + (t.stop_loss||'—') + '
盈' + (t.take_profit||'—') + '
' + (t.price_at_track||'—') + '' + (t.position_advice||'—') + '' + tPnl + '' + actualInfo + '' + statusBadge(t.status) + (t.close_reason ? '
' + t.close_reason + '' : '') + '
+
`; + evalTimer = setInterval(renderEvaluation, 30000); }); } function triggerEval() { @@ -1902,4 +1877,4 @@ function inMd(t) { } - \ No newline at end of file +