refactor(D组): 归档strategy_evaluator+advice_reconciliation,功能合并到strategy_effectiveness

- 归档strategy_evaluator.py/advice_reconciliation.py(功能已被strategy_effectiveness替代)
- strategy_effectiveness扩展: 读取recommendation_log+execution_log增强评估
- mofin_db新增: log_recommendation/log_execution/match_recommendations_executions
- 新表recommendation_log(推荐历史)+execution_log(执行历史)
This commit is contained in:
xxm
2026-08-21 02:25:06 +08:00
parent 3e7c7df38d
commit 83e2953f89
5 changed files with 112 additions and 0 deletions
@@ -1,245 +0,0 @@
#!/usr/bin/env python3
"""advice_reconciliation.py — 建议对账脚本
每周运行一次,对比 decisions.json 的 advice_timeline 与 portfolio.json
的实际持仓变化,统计准确率。
用法:
python3 advice_reconciliation.py # 正常对账
python3 advice_reconciliation.py --force # 强制重新对账所有建议
"""
import json
import sys
from datetime import datetime, timedelta
from pathlib import Path
from mo_data import read_decisions, read_portfolio
from mofin_db import get_conn, write_holding_strategy
ACCURACY_PATH = Path(__file__).parent.parent / "data" / "accuracy_stats.json"
def load_json(path, default=None):
try:
with open(path, encoding="utf-8") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {} if default is None else default
def save_json(path, data):
Path(path).parent.mkdir(parents=True, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def get_holding_change(portfolio, code):
"""获取某只股票的当前持仓信息"""
holdings = portfolio.get("holdings", [])
for h in holdings:
if h["code"] == code:
return {
"code": code,
"name": h.get("name", ""),
"shares": h.get("shares", 0),
"cost": h.get("cost", 0),
"price": h.get("price", 0),
"position_pct": h.get("position_pct", 0),
}
return None # 已清仓
def evaluate_advice(advice, current_holding):
"""评估一条建议是否正确
Returns: 'correct', 'partial', 'wrong', 'pending', 'unknown'
"""
direction = advice.get("direction", "")
status = advice.get("status", "pending")
if status == "ignored":
return "ignored"
if status == "pending":
return "pending"
if not current_holding:
# 股票已清仓
if direction in ("卖出", "清仓", "减仓"):
return "correct"
elif direction in ("买入", "加仓", "补仓"):
return "wrong"
else:
return "unknown"
shares = current_holding.get("shares", 0)
cost = current_holding.get("cost", 0)
price = current_holding.get("price", 0)
pnl_pct = (price - cost) / cost * 100 if cost > 0 else 0
if direction in ("买入", "加仓", "补仓"):
# 如果建议买入时价格低于现价,或浮盈为正 → 正确
try:
advised_price = float(advice.get("price", 0))
if advised_price > 0 and price > 0:
if price >= advised_price * 0.95: # 允许5%误差
return "correct"
else:
return "wrong"
else:
return "unknown"
except:
return "unknown"
elif direction in ("卖出", "清仓", "减仓"):
# 如果建议卖出时价格高于现价 → 正确(规避了下跌)
try:
advised_price = float(advice.get("price", 0))
if advised_price > 0 and price > 0:
if price <= advised_price * 1.05:
return "correct"
else:
return "wrong"
else:
return "unknown"
except:
return "unknown"
elif direction in ("持有", "观望"):
# 持有建议 → 看后续是否涨
try:
advised_price = float(advice.get("price", 0))
if advised_price > 0 and price > 0:
change = (price - advised_price) / advised_price * 100
if change > -5: # 没跌超过5%
return "correct"
else:
return "wrong"
else:
return "unknown"
except:
return "unknown"
elif direction == "自选":
# 自选建议无法直接对账
return "unknown"
return "unknown"
def run():
force = "--force" in sys.argv
decisions = read_decisions()
portfolio = read_portfolio()
old_stats = load_json(ACCURACY_PATH, {})
results = []
total = {"correct": 0, "wrong": 0, "partial": 0, "unknown": 0, "pending": 0, "ignored": 0}
for entry in decisions.get("decisions", []):
code = entry["code"]
name = entry.get("name", code)
timeline = entry.get("advice_timeline", [])
if not timeline:
continue
current_holding = get_holding_change(portfolio, code)
for i, advice in enumerate(timeline):
# 跳过已评估过的(除非 --force)
if not force and advice.get("evaluated"):
# 计数已有结果
result = advice.get("result", "unknown")
total[result] = total.get(result, 0) + 1
continue
result = evaluate_advice(advice, current_holding)
advice["evaluated"] = True
advice["result"] = result
advice["evaluated_at"] = datetime.now().isoformat()
total[result] = total.get(result, 0) + 1
results.append({
"code": code,
"name": name,
"date": advice.get("date", ""),
"direction": advice.get("direction", ""),
"summary": advice.get("summary", ""),
"result": result,
})
# 保存更新后的 decisions 到 DB(含评估标记)
conn = get_conn()
for entry in decisions.get("decisions", []):
code = entry.get("code", "")
name = entry.get("name", code)
write_holding_strategy(conn, code, name, entry)
# 写入 advice_timeline 评估标记
for adv in entry.get("advice_timeline", []):
conn.execute(
"""INSERT OR REPLACE INTO advice_timeline
(id, code, date, direction, price, summary, status,
evaluated, result, evaluated_at, report_id)
VALUES (
(SELECT id FROM advice_timeline WHERE code=? AND date=? AND direction=? AND summary=?),
?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)""",
(code, adv.get("date", ""), adv.get("direction", ""), adv.get("summary", ""),
code, adv.get("date", ""), adv.get("direction", ""),
adv.get("price"), adv.get("summary", ""), adv.get("status", ""),
1 if adv.get("evaluated") else 0,
adv.get("result", ""), adv.get("evaluated_at", ""),
adv.get("report_id", "")))
conn.commit()
conn.close()
# 计算准确率
evaluated = total["correct"] + total["wrong"] + total["partial"]
accuracy = round(total["correct"] / evaluated * 100, 1) if evaluated > 0 else 0
stats = {
"updated_at": datetime.now().isoformat(),
"period_start": old_stats.get("period_start", (datetime.now() - timedelta(days=7)).isoformat()),
"period_end": datetime.now().isoformat(),
"total_advice": sum(total.values()),
"correct": total["correct"],
"wrong": total["wrong"],
"partial": total["partial"],
"unknown": total["unknown"],
"pending": total["pending"],
"ignored": total["ignored"],
"evaluated": evaluated,
"accuracy_pct": accuracy,
"details": results,
# 累计统计
"cumulative": {
"total": old_stats.get("cumulative", {}).get("total", 0) + evaluated,
"correct": old_stats.get("cumulative", {}).get("correct", 0) + total["correct"],
"wrong": old_stats.get("cumulative", {}).get("wrong", 0) + total["wrong"],
},
}
cum = stats["cumulative"]
cum_accuracy = round(cum["correct"] / cum["total"] * 100, 1) if cum["total"] > 0 else 0
stats["cumulative_accuracy_pct"] = cum_accuracy
save_json(ACCURACY_PATH, stats)
# 输出摘要
print(f"📊 建议对账报告")
print(f" 周期: {stats['period_start'][:10]} ~ {stats['period_end'][:10]}")
print(f" 总建议: {stats['total_advice']}")
print(f" ✅ 正确: {stats['correct']}")
print(f" ❌ 错误: {stats['wrong']}")
print(f" ⏳ 待确认: {stats['pending']}")
print(f" ✗ 已忽略: {stats['ignored']}")
print(f" ❓ 无法判断: {stats['unknown']}")
print(f" 📈 本期准确率: {accuracy}%")
print(f" 📈 累计准确率: {cum_accuracy}%")
if results:
print(f"\n 详情:")
for r in results[:20]:
icon = {"correct": "", "wrong": "", "partial": "🟡", "unknown": "", "pending": "", "ignored": ""}
print(f" {icon.get(r['result'], '?')} {r['name']}({r['code']}) {r['direction']}{r['result']}")
if __name__ == "__main__":
run()
+69
View File
@@ -2486,3 +2486,72 @@ def write_holding_strategy_versioned(conn, code, name, data, source_trigger="bat
data['version'] = new_ver
return write_holding_strategy(conn, code, name, data, source_trigger)
# ── recommendation_log 写入(重评时同步记录推荐)──
def log_recommendation(conn, strategy_id, code, data, source_trigger="batch_12d"):
"""记录推荐历史:重评写入策略卡时同步调用"""
now = __import__('datetime').datetime.now().isoformat()
conn.execute("""
INSERT INTO recommendation_log
(strategy_id, code, strategy_source, version, recommend_time,
action, entry_low, entry_high, stop_loss, take_profit,
position_advice, timing_signal, rr_ratio, reason)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
""", (strategy_id, code, data.get('strategy_source', ''), data.get('version', ''),
now, data.get('action', ''), data.get('entry_low', 0), data.get('entry_high', 0),
data.get('stop_loss', 0), data.get('take_profit', 0),
data.get('position_advice', ''), data.get('timing_signal', ''),
data.get('rr_ratio', 0), data.get('reason', '')))
print(f" [RECOMMEND] {code} v{data.get('version','')} 记录推荐", flush=True)
# ── execution_log 写入(交易执行时记录)──
def log_execution(conn, code, action, shares, price, cost=None, source="trade_capture", recommendation_id=None):
"""记录执行历史:trade_capture/import_holding_xls 等执行后调用"""
now = __import__('datetime').datetime.now().isoformat()
conn.execute("""
INSERT INTO execution_log
(code, action, shares, price, cost, execute_time, source, match_recommendation_id)
VALUES (?,?,?,?,?,?,?,?)
""", (code, action, shares, price, cost, now, source, recommendation_id))
print(f" [EXEC] {code} {action} {shares}股 @{price} source={source}", flush=True)
# ── 匹配推荐与执行(盘后评估用)──
def match_recommendations_executions(conn, code, strategy_id):
"""找到策略推荐与实际执行的对应关系"""
recs = conn.execute(
"SELECT id, recommend_time, action, entry_low, entry_high FROM recommendation_log "
"WHERE code=? AND strategy_id=? ORDER BY recommend_time", (code, strategy_id)).fetchall()
execs = conn.execute(
"SELECT id, execute_time, action, shares, price FROM execution_log "
"WHERE code=? ORDER BY execute_time", (code,)).fetchall()
matches = []
for rec in recs:
rec_time = rec[1]
rec_action = rec[2]
# 找推荐之后最近的执行
for exe in execs:
if exe[1] >= rec_time and exe[2] == rec_action:
matches.append({
"recommendation_id": rec[0],
"recommend_time": rec[1],
"recommend_action": rec[2],
"execution_id": exe[0],
"execute_time": exe[1],
"execute_action": exe[2],
"shares": exe[3],
"price": exe[4],
"matched": True,
})
break
else:
matches.append({
"recommendation_id": rec[0],
"recommend_time": rec[1],
"recommend_action": rec[2],
"matched": False,
})
return matches
@@ -241,3 +241,43 @@ def main():
if __name__ == "__main__":
main()
# ── 扩展评估:读取 recommendation_log + execution_log ──
def evaluate_with_logs(conn, strategy):
"""用推荐记录+执行记录增强评估"""
sid = strategy[0]
code = strategy[1]
# 读推荐记录
recs = conn.execute(
"SELECT recommend_time, action, entry_low, entry_high, stop_loss, take_profit "
"FROM recommendation_log WHERE strategy_id=? ORDER BY recommend_time", (sid,)).fetchall()
# 读执行记录
execs = conn.execute(
"SELECT execute_time, action, shares, price FROM execution_log "
"WHERE code=? ORDER BY execute_time", (code,)).fetchall()
# 匹配:推荐后是否有对应执行
rec_followed = 0
rec_not_followed = 0
for rec in recs:
rec_time, rec_action = rec[0], rec[1]
found = False
for exe in execs:
if exe[0] >= rec_time and exe[1] == rec_action:
found = True
rec_followed += 1
break
if not found:
rec_not_followed += 1
compliance_rate = rec_followed / (rec_followed + rec_not_followed) * 100 if (rec_followed + rec_not_followed) > 0 else 0
return {
"recommendations": len(recs),
"executions_matched": rec_followed,
"executions_missed": rec_not_followed,
"compliance_rate": round(compliance_rate, 1),
}
@@ -1,535 +0,0 @@
#!/usr/bin/env python3
"""strategy_evaluator.py — 策略双维度评估引擎
两阶段评估模型:
阶段一(策略制定→价格达标):
理论:策略设定的买入区/止损/止盈 → 股价是否达到过这些价位 → 理论盈亏
实际:老爸是否按策略执行 → 实际买入/卖出价格 → 实际盈亏
阶段二(价格回落后→新止损验证):
理论:价格未按预期走 → 给出新止损 → 股价是否继续下跌验证止损正确性
实际:老爸实际卖出价格 → 对比新止损 → 验证止损有效性
输出:写入 decisions.json 的 evaluation 字段 + accuracy_stats.json
"""
import json
import os
import sys
import re
from datetime import datetime, timedelta
from pathlib import Path
from mo_data import read_decisions, read_portfolio, get_price, get_prices_batch
from mofin_db import get_conn, write_holding_strategy
DATA_DIR = Path(__file__).parent.parent / "data"
ACCURACY_PATH = DATA_DIR / "accuracy_stats.json"
def load_json(path, default=None):
try:
with open(path, encoding="utf-8") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {} if default is None else default
def save_json(path, data):
Path(path).parent.mkdir(parents=True, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def fetch_prices(codes):
"""批量拉价格。DB 优先(price_monitor 维护),腾讯 API fallback"""
if not codes:
return {}
# 主通道: DB
try:
from mofin_db import get_prices_batch_from_db
db_results = get_prices_batch_from_db(codes)
if db_results:
return {code: {"name": "", "price": p, "prev_close": 0, "change_pct": chg or 0,
"high": 0, "low": 0} for code, (p, chg) in db_results.items()}
except Exception:
pass
# Fallback: mo_data.get_prices_batch
try:
raw = get_prices_batch(codes)
if raw:
return {code: {"name": "", "price": p, "prev_close": 0, "change_pct": chg or 0,
"high": 0, "low": 0} for code, (p, chg) in raw.items()}
except Exception as e:
print(f"行情拉取失败: {e}", file=sys.stderr)
return {}
def parse_tech_snapshot(decision):
"""
从 decision 的 tech_snapshot 中提取技术面参数。
返回 dict: { 'pattern', 'volume', '强撑', '弱撑', '弱压', '强压' }
"""
trig = decision.get("trigger", {})
raw = trig.get("tech_snapshot", decision.get("tech_snapshot", ""))
result = {}
if not raw:
return result
# 形态:XXX/bullish 或 形态:XXX/neutral
m = re.search(r'形态:([^\s]+)', raw)
if m:
result["pattern"] = m.group(1)
# 量价:XXX
m = re.search(r'量价:([^\s]+)', raw)
if m:
result["volume"] = m.group(1)
# 强撑:N / 弱撑:N / 弱压:N / 强压:N
for key in ["强撑", "弱撑", "弱压", "强压"]:
m = re.search(rf'{key}:([\d.]+)', raw)
if m:
result[key] = float(m.group(1))
return result
def build_strategy_rationale(sl_p, tp_p, tech, decision, actual_pnl_pct):
"""
基于 tech_snapshot 的支撑/压力位,解释止损和止盈的设定依据。
返回 dict:
- sl_basis: 止损设定依据(对应哪个支撑位)
- tp_basis: 止盈设定依据(对应哪个压力位)
- analysis: 综合技术面文字分析
- tech_used: 实际使用的 tech 参数
"""
code = decision.get("code", "")
name = decision.get("name", code)
rationale = {
"sl_basis": "未设定",
"tp_basis": "未设定",
"analysis": "",
"tech_used": tech,
}
# 解析支撑/压力
强撑 = tech.get("强撑")
弱撑 = tech.get("弱撑")
弱压 = tech.get("弱压")
强压 = tech.get("强压")
形态 = tech.get("pattern", "无记录")
量价 = tech.get("volume", "数据不足")
# ---- 止损依据 ----
if sl_p and 强撑 and 弱撑:
# 止损在强撑附近 (±1%)
if abs(sl_p - 强撑) / 强撑 < 0.02:
rationale["sl_basis"] = f"技术面强支撑{强撑}(距{abs(sl_p-强撑)/强撑*100:.1f}%"
# 止损在弱撑附近
elif abs(sl_p - 弱撑) / 弱撑 < 0.02:
rationale["sl_basis"] = f"技术面弱支撑{弱撑}(距{abs(sl_p-弱撑)/弱撑*100:.1f}%"
# 止损在强撑和弱撑之间
elif 强撑 < sl_p < 弱撑:
diff_down = (sl_p - 强撑) / 强撑 * 100
diff_up = (弱撑 - sl_p) / sl_p * 100
rationale["sl_basis"] = f"技术面强撑{强撑}-弱撑{弱撑}之间(比强撑高{diff_down:.1f}%,比弱撑低{diff_up:.1f}%"
# 止损低于强撑(宽止损,多见于深套)
elif sl_p < 强撑:
diff = (强撑 - sl_p) / sl_p * 100
actual = actual_pnl_pct if actual_pnl_pct else 0
if actual < -20:
rationale["sl_basis"] = f"低于技术面强撑{强撑}{diff:.1f}%(深套宽止损)"
else:
rationale["sl_basis"] = f"低于技术面强撑{强撑}{diff:.1f}%(宽止损)"
# 止损高于弱撑(紧止损)
elif sl_p > 弱撑:
rationale["sl_basis"] = f"高于弱撑{弱撑}(紧止损)"
elif sl_p and 强撑 and not 弱撑:
if abs(sl_p - 强撑) / 强撑 < 0.02:
rationale["sl_basis"] = f"技术面强支撑{强撑}"
else:
rationale["sl_basis"] = f"参考强撑{强撑}调整至{sl_p}"
elif sl_p:
rationale["sl_basis"] = f"直接设定为{sl_p}(无技术面支撑位参考)"
# ---- 止盈依据 ----
if tp_p and 强压:
if abs(tp_p - 强压) / 强压 < 0.03:
rationale["tp_basis"] = f"技术面强压力{强压}(距{abs(tp_p-强压)/强压*100:.1f}%"
elif tp_p > 强压:
diff = (tp_p - 强压) / 强压 * 100
rationale["tp_basis"] = f"技术面强压{强压}上方{diff:.1f}%(趋势延伸目标)"
elif 弱压 and 弱压 < tp_p < 强压:
rationale["tp_basis"] = f"技术面弱压{弱压}-强压{强压}之间"
elif 弱压 and tp_p <= 弱压:
rationale["tp_basis"] = f"接近弱压{弱压}(保守目标)"
else:
rationale["tp_basis"] = f"参考强压{强压}调整至{tp_p}"
elif tp_p and not 强压:
rationale["tp_basis"] = f"直接设定为{tp_p}(无技术面压力位参考)"
elif not tp_p:
rationale["tp_basis"] = "未设定止盈价"
# ---- 综合技术面分析 ----
parts = []
if 形态:
parts.append(f"K线形态:{形态}")
if 量价:
parts.append(f"量价:{量价}")
if 强撑 or 弱撑 or 弱压 or 强压:
levels = []
if 强撑: levels.append(f"强撑{强撑}")
if 弱撑: levels.append(f"弱撑{弱撑}")
if 弱压: levels.append(f"弱压{弱压}")
if 强压: levels.append(f"强压{强压}")
parts.append("技术位:" + "/".join(levels))
rationale["analysis"] = " | ".join(parts)
return rationale
def evaluate_phase1(decision, price_info, holding):
"""
阶段一评估:策略制定→价格是否达到过目标价位
返回 evaluation dict
"""
trig = decision.get("trigger", {})
code = decision["code"]
name = decision.get("name", code)
price = price_info.get("price", 0)
change = price_info.get("change_pct", 0)
# 策略区间 — 支持两种数据格式:
# 1) trigger 子对象(含 entry_zone/stop_loss/take_profit
# 2) 顶层字段(entry_low+entry_high / stop_loss / take_profit
el = trig.get("entry_zone", "")
sl = trig.get("stop_loss", "")
tp = trig.get("take_profit", "")
if not el and decision.get("entry_low") is not None:
el_low = decision.get("entry_low")
el_high = decision.get("entry_high")
el = f"{el_low}~{el_high}" if el_low is not None and el_high is not None else ""
if not sl:
sl = decision.get("stop_loss", "")
if not tp:
tp = decision.get("take_profit", "")
el_low = el_high = None
if el and "~" in str(el):
try:
parts = str(el).split("~")
el_low, el_high = float(parts[0]), float(parts[1])
except:
pass
sl_p = float(sl) if sl else None
tp_p = float(tp) if tp else None
# 持仓信息
cost = holding.get("cost", 0) if holding else 0
shares = holding.get("shares", 0) if holding else 0
position_pct = holding.get("position_pct", 0) if holding else 0
# 理论盈亏计算(基于策略区间中值)
entry_mid = (el_low + el_high) / 2 if el_low and el_high else price
theoretical_pnl_pct = (tp_p - entry_mid) / entry_mid * 100 if tp_p and entry_mid else 0
theoretical_pnl_amount = theoretical_pnl_pct / 100 * entry_mid * (shares or 100) / 100 if shares else 0
# 实际盈亏
actual_pnl_pct = (price - cost) / cost * 100 if cost > 0 and price > 0 else 0
actual_pnl_amount = actual_pnl_pct / 100 * cost * shares if cost > 0 and shares > 0 else 0
# === 策略依据分析(2026-06-18 新增)===
tech = parse_tech_snapshot(decision)
rationale = build_strategy_rationale(sl_p, tp_p, tech, decision, actual_pnl_pct)
# === R/R 盈亏比计算(2026-06-18 新增)===
# 以现价为基准计算:向下风险(到止损)vs 向上空间(到止盈)
# 止损价作为风险基准,止盈价作为收益目标
rr = None
rr_risk_pct = None
rr_reward_pct = None
rr_interpretation = ""
rr_level = ""
if sl_p and tp_p and price > 0 and sl_p > 0 and price > sl_p:
rr_risk_pct = round((price - sl_p) / price * 100, 2) # 距止损%
rr_reward_pct = round((tp_p - price) / price * 100, 2) # 距止盈%
rr = round(rr_reward_pct / rr_risk_pct, 2) if rr_risk_pct > 0 else None
# 判断场景:深套/已持仓盈利/已持仓亏损/新买入
is_deep_loss = actual_pnl_pct < -20
has_profit = actual_pnl_pct >= 0
held = (cost > 0 and shares > 0)
if not held:
# 新买入/自选股场景
if rr is not None and rr < 1.5:
rr_level = "⚠️盈亏比不足"
rr_interpretation = f"新买入要求R/R≥1.5,现{rr}每亏1元仅赚{rr}元,不建议买入"
elif rr is not None and rr < 2.0:
rr_level = "⚠️盈亏比偏低"
rr_interpretation = f"新买入要求R/R≥1.5,现{rr}每亏1元赚{rr}元,谨慎"
else:
rr_level = "R/R达标"
rr_interpretation = f"每亏1元赚{rr}元,盈亏比合理"
elif is_deep_loss:
# 深套场景 — 不限R/R
rr_level = "深套持有"
rr_interpretation = f"浮亏{actual_pnl_pct:.1f}%>20%深套,R/R={rr}仅参考,不补不割等反弹"
elif has_profit:
# 已持仓盈利场景
if rr is not None and rr < 0.5:
rr_level = "⚠️R/R极低"
rr_interpretation = f"盈利持仓R/R={rr},每亏1元仅赚{rr}元,考虑止盈或上移止损保护利润"
elif rr is not None and rr < 1.5:
rr_level = "⚠️R/R偏低"
rr_interpretation = f"盈利持仓R/R={rr},不建议加仓,当前仓位持有观察"
else:
rr_level = "R/R合理"
rr_interpretation = f"盈利持仓R/R={rr},每亏1元赚{rr}元,持有合理"
else:
# 已持仓浮亏(但非深套)
if rr is not None and rr < 0.5:
rr_level = "⚠️R/R极低"
rr_interpretation = f"浮亏持仓R/R={rr},每亏1元仅赚{rr}元,不建议加仓,关注止损"
elif rr is not None and rr < 1.0:
rr_level = "⚠️R/R不足"
rr_interpretation = f"浮亏持仓要求加仓R/R≥1.0,现{rr},不加仓"
else:
rr_level = "R/R可接受"
rr_interpretation = f"浮亏持仓R/R={rr},每亏1元赚{rr}元,持有等反弹"
elif price and sl_p and price <= sl_p:
rr_level = "已跌破止损"
rr_interpretation = f"现价{price}已破止损{sl_p}R/R不适用"
else:
rr_interpretation = "止损或止盈缺失,无法计算R/R"
# 当前状态判断
status = "safe"
if sl_p and price > 0 and price <= sl_p:
status = "stop_loss_hit"
elif tp_p and price > 0 and price >= tp_p:
status = "take_profit_hit"
elif el_low and el_high and price > 0 and el_low <= price <= el_high:
status = "in_entry_zone"
elif el_low and price > 0 and price < el_low:
status = "below_entry"
elif el_high and price > 0 and price > el_high:
status = "above_entry"
# 理论阶段评估
theoretical = {
"entry_zone": f"{el_low}~{el_high}" if el_low else "N/A",
"stop_loss": sl_p,
"take_profit": tp_p,
"entry_mid_price": round(entry_mid, 2),
"target_price": tp_p,
"theoretical_pnl_pct": round(theoretical_pnl_pct, 2),
"theoretical_pnl_amount": round(theoretical_pnl_amount, 2),
"status": status,
"current_price": price,
"current_change_pct": change,
"rr": rr,
"rr_risk_pct": rr_risk_pct,
"rr_reward_pct": rr_reward_pct,
"rr_level": rr_level,
"sl_basis": rationale["sl_basis"],
"tp_basis": rationale["tp_basis"],
"tech_analysis": rationale["analysis"],
}
# 实际阶段评估
actual = {
"cost_price": cost,
"shares": shares,
"position_pct": position_pct,
"actual_pnl_pct": round(actual_pnl_pct, 2),
"actual_pnl_amount": round(actual_pnl_amount, 2),
"status": status,
"current_price": price,
}
return {
"code": code,
"name": name,
"evaluated_at": datetime.now().isoformat(),
"phase": 1,
"theoretical": theoretical,
"actual": actual,
"rr_level": rr_level,
"rr_interpretation": rr_interpretation,
"strategy_rationale": {
"sl_basis": rationale["sl_basis"],
"tp_basis": rationale["tp_basis"],
"tech_analysis": rationale["analysis"],
},
"summary": f"{name}({code}) | 损{sl_p}({rationale['sl_basis']})/"
f"{tp_p}({rationale['tp_basis']}) | "
f"现价{price}({change:+.2f}%) | "
f"距损{rr_risk_pct}%/距盈{rr_reward_pct}% | RR={rr} | "
f"{rr_level} | 理{theoretical_pnl_pct:+.1f}%实{actual_pnl_pct:+.1f}%",
}
def evaluate_phase2(decision, price_info, holding, prev_eval):
"""
阶段二评估:价格回落后→新止损验证
需要 prev_eval 中记录了之前的目标价和新止损价
"""
trig = decision.get("trigger", {})
code = decision["code"]
name = decision.get("name", code)
price = price_info.get("price", 0)
sl = trig.get("stop_loss", "")
if not sl:
sl = decision.get("stop_loss", "")
sl_p = float(sl) if sl else None
# 从 prev_eval 中获取阶段一的止损
prev_sl = None
if prev_eval:
prev_sl = prev_eval.get("theoretical", {}).get("stop_loss")
# 检查新止损是否被跌破
new_sl_hit = False
days_to_hit = None
if sl_p and price > 0 and price <= sl_p:
new_sl_hit = True
# 无法精确知道多少天跌破,标记为当前
days_to_hit = 0
result = {
"code": code,
"name": name,
"evaluated_at": datetime.now().isoformat(),
"phase": 2,
"new_stop_loss": sl_p,
"previous_stop_loss": prev_sl,
"current_price": price,
"new_sl_hit": new_sl_hit,
"days_to_hit": days_to_hit,
"summary": f"{name}({code}) 新止损{sl_p} {'已跌破' if new_sl_hit else '未触及'} 现价{price}",
}
return result
def run():
decisions = read_decisions()
portfolio = read_portfolio()
holdings_map = {h["code"]: h for h in portfolio.get("holdings", [])}
# 收集所有代码
all_codes = [d["code"] for d in decisions["decisions"]]
prices = fetch_prices(all_codes)
results = []
stats = {
"phase1_correct": 0, "phase1_wrong": 0, "phase1_pending": 0,
"phase2_correct": 0, "phase2_wrong": 0, "phase2_pending": 0,
}
for d in decisions["decisions"]:
code = d["code"]
pi = prices.get(code, {})
h = holdings_map.get(code)
# 获取已有的 evaluation 记录
existing_eval = d.get("evaluation", [])
# 阶段一评估
eval1 = evaluate_phase1(d, pi, h)
results.append(eval1)
# 阶段二评估(如果有前次止损记录)
prev_eval = existing_eval[-1] if existing_eval else None
if prev_eval and prev_eval.get("phase") == 1:
eval2 = evaluate_phase2(d, pi, h, prev_eval)
results.append(eval2)
# 更新 decisions.json 的 evaluation 字段
d["evaluation"] = [e for e in [eval1] + ([eval2] if prev_eval and prev_eval.get("phase") == 1 else [])]
# 保存更新到 DB
conn = get_conn()
for d in decisions.get("decisions", []):
write_holding_strategy(conn, d["code"], d.get("name", ""), d)
conn.close()
# 汇总统计
for r in results:
phase = r["phase"]
if phase == 1:
status = r["theoretical"]["status"]
if status in ("take_profit_hit",):
stats["phase1_correct"] += 1
elif status in ("stop_loss_hit",):
stats["phase1_wrong"] += 1
else:
stats["phase1_pending"] += 1
elif phase == 2:
if r.get("new_sl_hit"):
stats["phase2_correct"] += 1
else:
stats["phase2_pending"] += 1
# 写入 accuracy_stats
accuracy = {
"updated_at": datetime.now().isoformat(),
"phase1": {
"correct": stats["phase1_correct"],
"wrong": stats["phase1_wrong"],
"pending": stats["phase1_pending"],
"accuracy_pct": round(stats["phase1_correct"] / max(stats["phase1_correct"] + stats["phase1_wrong"], 1) * 100, 1),
},
"phase2": {
"correct": stats["phase2_correct"],
"wrong": stats["phase2_wrong"],
"pending": stats["phase2_pending"],
"accuracy_pct": round(stats["phase2_correct"] / max(stats["phase2_correct"] + stats["phase2_wrong"], 1) * 100, 1),
},
"total_evaluated": len(results),
"details": [r["summary"] for r in results],
}
save_json(ACCURACY_PATH, accuracy)
# 输出报告
print("=" * 70)
print(f"策略双维度评估报告 | {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 70)
print(f"\n📊 阶段一(策略制定→价格达标)")
print(f" 正确(达到止盈): {stats['phase1_correct']}")
print(f" 错误(跌破止损): {stats['phase1_wrong']}")
print(f" 待验证: {stats['phase1_pending']}")
print(f" 准确率: {accuracy['phase1']['accuracy_pct']}%")
print(f"\n📊 阶段二(价格回落→新止损验证)")
print(f" 正确(新止损验证有效): {stats['phase2_correct']}")
print(f" 错误: {stats['phase2_wrong']}")
print(f" 待验证: {stats['phase2_pending']}")
print(f"\n📋 逐股评估:")
for r in results:
if r["phase"] == 1:
print(f" {r['summary']}")
if r.get("rr_interpretation"):
print(f" RR: {r['rr_interpretation']}")
sr = r.get("strategy_rationale", {})
if sr.get("tech_analysis"):
print(f" 技术:{sr['tech_analysis']}")
else:
print(f" {r['summary']}")
# R/R 统计
rr_count = sum(1 for r in results if r.get("rr_level") and r["phase"] == 1)
rr_warn = sum(1 for r in results if "⚠️" in r.get("rr_level", "") and r["phase"] == 1)
print(f"\n📊 盈亏比R/R统计:")
print(f" 有R/R评估: {rr_count}只 | ⚠️异常: {rr_warn}")
for r in results:
if r["phase"] == 1 and r.get("rr_level"):
level = r["rr_level"]
if "⚠️" in level:
name_code = r['summary'].split('|')[0].strip()
print(f" ⚠️ {name_code}{level} | {r['rr_interpretation']}")
print(f"\n✅ 评估完成,已写入 decisions.json 和 accuracy_stats.json")
if __name__ == "__main__":
run()
+3
View File
@@ -150,6 +150,9 @@ def execute_stock(stock):
pass
if not result.get("action"):
result["action"] = "无变更"
# 记录执行日志
from mofin_db import log_execution
log_execution(conn, code, result.get("action",""), int(stock.get("shares",0) or 0), float(stock.get("price",0) or 0), source="trade_capture")
conn.commit()
conn.close()