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:
@@ -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 默认
|
||||
|
||||
Reference in New Issue
Block a user