From bc50f300ba0590f600bb5bf18a6c61d2cf188bfa Mon Sep 17 00:00:00 2001 From: dekun Date: Sat, 17 Oct 2026 22:54:19 +0800 Subject: [PATCH] Define entry styles: trend/reversal x 3 patterns, plus messy other. Co-authored-by: Cursor --- app/db.py | 50 +++++++++++++++++++++++++++- app/routes.py | 14 +++++++- app/templates/record_detail.html | 26 ++++++++++++--- app/templates/records_stats.html | 57 ++++++++++++++++++++++++++++++-- 4 files changed, 139 insertions(+), 8 deletions(-) diff --git a/app/db.py b/app/db.py index 706eefa..db5b192 100644 --- a/app/db.py +++ b/app/db.py @@ -304,6 +304,28 @@ def list_trades( return rows +# 入场形式:顺势/反转各含三种结构手法;其它=乱作 +ENTRY_PATTERN_TYPES = ("窄幅区间突破", "宽幅区间假突破", "波段高低点假突破") +ENTRY_STYLES = ( + [f"顺势 · {p}" for p in ENTRY_PATTERN_TYPES] + + [f"反转 · {p}" for p in ENTRY_PATTERN_TYPES] + + ["其他 · 乱作"] +) + + +def entry_category(style: str) -> str: + s = (style or "").strip() + if not s: + return "未标注" + if s.startswith("顺势"): + return "顺势" + if s.startswith("反转"): + return "反转" + if s.startswith("其他") or "乱作" in s: + return "其他" + return "旧分类" + + def _hold_seconds(open_time: Any, close_time: Any, now_ts: Optional[float] = None) -> Optional[float]: if open_time in (None, ""): return None @@ -410,6 +432,10 @@ def trade_stats( by_hold = {k: _empty_group(v) for k, v in bucket_labels.items()} by_machine: dict[str, dict[str, Any]] = {} by_side: dict[str, dict[str, Any]] = {} + by_category: dict[str, dict[str, Any]] = { + k: _empty_group(k) for k in ("顺势", "反转", "其他", "未标注", "旧分类") + } + by_style: dict[str, dict[str, Any]] = {s: _empty_group(s) for s in ENTRY_STYLES} total = len(rows) closed = 0 @@ -447,9 +473,20 @@ def trade_stats( hold_sum += hold hold_n += 1 - if (t.get("entry_style") or "").strip(): + style = (t.get("entry_style") or "").strip() + if style: annotated += 1 + cat = entry_category(style) + _accumulate(by_category[cat], profit, hold) + if style in by_style: + _accumulate(by_style[style], profit, hold) + elif style: + key = f"旧 · {style}" + if key not in by_style: + by_style[key] = _empty_group(key) + _accumulate(by_style[key], profit, hold) + bk = _bucket_key(hold) _accumulate(by_hold[bk], profit, hold) @@ -467,6 +504,7 @@ def trade_stats( decided = wins + losses avg_hold = (hold_sum / hold_n) if hold_n else None + style_order = list(ENTRY_STYLES) + [k for k in by_style if k not in ENTRY_STYLES] return { "total": total, "closed": closed, @@ -491,6 +529,16 @@ def trade_stats( reverse=True, ), "by_side": [_finalize_group(g) for g in by_side.values()], + "by_category": [ + _finalize_group(by_category[k]) + for k in ("顺势", "反转", "其他", "未标注", "旧分类") + if by_category[k]["count"] or k in ("顺势", "反转", "其他", "未标注") + ], + "by_style": [ + _finalize_group(by_style[k]) + for k in style_order + if k in by_style and (by_style[k]["count"] or k in ENTRY_STYLES) + ], } diff --git a/app/routes.py b/app/routes.py index 1c99720..ab44ef7 100644 --- a/app/routes.py +++ b/app/routes.py @@ -699,7 +699,13 @@ def register_routes(app: Flask) -> None: charts[tf] = database.get_bars( conn, trade["symbol"], tf, float(center) - window, float(center) + window ) - return render_template("record_detail.html", trade=trade, charts=charts) + return render_template( + "record_detail.html", + trade=trade, + charts=charts, + entry_styles=database.ENTRY_STYLES, + entry_patterns=database.ENTRY_PATTERN_TYPES, + ) @app.post("/records//annotate") @login_required @@ -707,11 +713,17 @@ def register_routes(app: Flask) -> None: u = current_user() entry_style = (request.form.get("entry_style") or "").strip() note = (request.form.get("review_note") or "").strip() + allowed = set(database.ENTRY_STYLES) with database.get_db() as conn: trade = database.get_trade(conn, tid) if not trade or not database.user_can_access_account(u, trade.get("account_id")): flash("无权标注该记录", "error") return redirect(url_for("records_page")) + # 允许清空;或保留已有旧标注值;新选必须在标准列表内 + prev = (trade.get("entry_style") or "").strip() + if entry_style and entry_style not in allowed and entry_style != prev: + flash("入场形式不在可选列表中", "error") + return redirect(url_for("record_detail", tid=tid)) conn.execute( "UPDATE trades SET entry_style=?, review_note=? WHERE id=?", (entry_style, note, tid), diff --git a/app/templates/record_detail.html b/app/templates/record_detail.html index bf158b6..057e4e6 100644 --- a/app/templates/record_detail.html +++ b/app/templates/record_detail.html @@ -27,11 +27,29 @@
+

顺势/反转均含:窄幅区间突破、宽幅区间假突破、波段高低点假突破;对不上的选「乱作」。

diff --git a/app/templates/records_stats.html b/app/templates/records_stats.html index 48aa7d6..aa6e6a3 100644 --- a/app/templates/records_stats.html +++ b/app/templates/records_stats.html @@ -2,7 +2,7 @@ {% block title %}统计看板 · {{ app_name }}{% endblock %} {% block content %}

统计看板

-

程序汇总成交数据;入场形式分类确定后再叠加维度。

+

程序汇总:顺势/反转 × 三种结构手法,其他=乱作。

交易列表 @@ -65,7 +65,60 @@
入场形式标注
{{ stats.annotated }} / {{ stats.total }}
-
未标注 {{ stats.unannotated }}(分类确定后再细拆)
+
未标注 {{ stats.unannotated }}
+
+
+ +
+
+

按大类(顺势 / 反转 / 其他)

+ + + + + + + + {% for g in stats.by_category %} + + + + + + + + {% else %} + + {% endfor %} + +
大类笔数胜率盈亏平均持仓
{{ g.label }}{{ g.count }}{{ '%.1f'|format(g.win_rate) }}% + {{ '%.2f'|format(g.profit) }} + {{ g.avg_hold }}
暂无数据
+
+
+

按入场形式(细项)

+ + + + + + + + {% for g in stats.by_style %} + + + + + + + + {% else %} + + {% endfor %} + +
形式笔数胜率盈亏平均持仓
{{ g.label }}{{ g.count }}{{ '%.1f'|format(g.win_rate) }}% + {{ '%.2f'|format(g.profit) }} + {{ g.avg_hold }}
暂无数据