Add lean review issues: up to two problem tags plus short note.

Persist review_issues, enforce max two on save, and show issue breakdown in stats.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-21 20:56:32 +08:00
parent 1bf353c505
commit 01ed885646
6 changed files with 165 additions and 10 deletions
+54 -1
View File
@@ -91,6 +91,7 @@ def init_db() -> None:
account_id INTEGER,
machine_id INTEGER,
entry_style TEXT DEFAULT '',
review_issues TEXT DEFAULT '',
review_note TEXT DEFAULT '',
snapshot_json TEXT DEFAULT '{}',
created_at REAL NOT NULL,
@@ -457,6 +458,35 @@ ENTRY_STYLES = (
+ ["其他 · 乱作"]
)
# 复盘主要问题(少而狠):最多勾 1~2 个
REVIEW_ISSUES = ("位不对", "形不对", "时机差", "出场违纪", "情绪单")
REVIEW_ISSUES_MAX = 2
def parse_review_issues(raw: Any) -> list[str]:
"""逗号分隔 → 有序列表,仅保留标准项。"""
if not raw:
return []
if isinstance(raw, (list, tuple)):
parts = [str(x).strip() for x in raw]
else:
parts = [p.strip() for p in str(raw).replace(",", ",").split(",")]
allowed = set(REVIEW_ISSUES)
seen: set[str] = set()
out: list[str] = []
for p in parts:
if p in allowed and p not in seen:
seen.add(p)
out.append(p)
# 按标准顺序排,方便展示一致
order = {name: i for i, name in enumerate(REVIEW_ISSUES)}
out.sort(key=lambda x: order.get(x, 99))
return out
def serialize_review_issues(items: Any) -> str:
return ",".join(parse_review_issues(items)[:REVIEW_ISSUES_MAX])
def entry_category(style: str) -> str:
s = (style or "").strip()
@@ -560,7 +590,7 @@ def trade_stats(
clause, params = _trades_where(account_id, machine_id)
sql = f"""
SELECT t.id, t.side, t.symbol, t.profit, t.open_time, t.close_time,
t.volume, t.machine_id, t.entry_style, t.snapshot_json,
t.volume, t.machine_id, t.entry_style, t.review_issues, t.snapshot_json,
m.name AS machine_name
FROM trades t
LEFT JOIN machines m ON m.id = t.machine_id
@@ -589,6 +619,8 @@ def trade_stats(
k: _empty_group(k) for k in ("顺势", "反转", "其他", "未标注", "旧分类")
}
by_style: dict[str, dict[str, Any]] = {s: _empty_group(s) for s in ENTRY_STYLES}
by_issue: dict[str, dict[str, Any]] = {s: _empty_group(s) for s in REVIEW_ISSUES}
issue_tagged = 0
rows: list[dict] = []
skipped_synthetic = 0
@@ -678,6 +710,10 @@ def trade_stats(
if style:
annotated += 1
issues = parse_review_issues(t.get("review_issues"))
if issues:
issue_tagged += 1
# 分组也只累加已平仓盈亏,避免浮盈干扰分类
cat = entry_category(style)
_accumulate(by_category[cat], profit, hold if closed_flag else None)
@@ -690,6 +726,10 @@ def trade_stats(
_accumulate(by_style[key], profit, hold if closed_flag else None)
if closed_flag:
for iss in issues:
if iss in by_issue:
_accumulate(by_issue[iss], profit, hold)
bk = _bucket_key(hold)
_accumulate(by_hold[bk], profit, hold)
@@ -727,6 +767,7 @@ def trade_stats(
"avg_hold": _fmt_hold_secs(avg_hold),
"annotated": annotated,
"unannotated": total - annotated,
"issue_tagged": issue_tagged,
"skipped_synthetic": skipped_synthetic,
"skipped_dup_ticket": skipped_dup_ticket,
"period": period,
@@ -738,6 +779,11 @@ def trade_stats(
reverse=True,
),
"by_side": [_finalize_group(g) for g in by_side.values()],
"by_issue": [
_finalize_group(by_issue[k])
for k in REVIEW_ISSUES
if by_issue[k]["count"]
],
"by_category": [
_finalize_group(by_category[k])
for k in ("顺势", "反转", "其他", "未标注", "旧分类")
@@ -858,6 +904,13 @@ def seed_if_empty() -> None:
except sqlite3.OperationalError:
pass
trade_cols = {r["name"] for r in conn.execute("PRAGMA table_info(trades)").fetchall()}
if "review_issues" not in trade_cols:
try:
conn.execute("ALTER TABLE trades ADD COLUMN review_issues TEXT DEFAULT ''")
except sqlite3.OperationalError:
pass
n_users = conn.execute("SELECT COUNT(*) AS c FROM users").fetchone()["c"]
if n_users == 0:
# 迁移旧 settings 密码,或使用 .env 默认