Define entry styles: trend/reversal x 3 patterns, plus messy other.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -304,6 +304,28 @@ def list_trades(
|
|||||||
return rows
|
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]:
|
def _hold_seconds(open_time: Any, close_time: Any, now_ts: Optional[float] = None) -> Optional[float]:
|
||||||
if open_time in (None, ""):
|
if open_time in (None, ""):
|
||||||
return None
|
return None
|
||||||
@@ -410,6 +432,10 @@ def trade_stats(
|
|||||||
by_hold = {k: _empty_group(v) for k, v in bucket_labels.items()}
|
by_hold = {k: _empty_group(v) for k, v in bucket_labels.items()}
|
||||||
by_machine: dict[str, dict[str, Any]] = {}
|
by_machine: dict[str, dict[str, Any]] = {}
|
||||||
by_side: 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)
|
total = len(rows)
|
||||||
closed = 0
|
closed = 0
|
||||||
@@ -447,9 +473,20 @@ def trade_stats(
|
|||||||
hold_sum += hold
|
hold_sum += hold
|
||||||
hold_n += 1
|
hold_n += 1
|
||||||
|
|
||||||
if (t.get("entry_style") or "").strip():
|
style = (t.get("entry_style") or "").strip()
|
||||||
|
if style:
|
||||||
annotated += 1
|
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)
|
bk = _bucket_key(hold)
|
||||||
_accumulate(by_hold[bk], profit, hold)
|
_accumulate(by_hold[bk], profit, hold)
|
||||||
|
|
||||||
@@ -467,6 +504,7 @@ def trade_stats(
|
|||||||
|
|
||||||
decided = wins + losses
|
decided = wins + losses
|
||||||
avg_hold = (hold_sum / hold_n) if hold_n else None
|
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 {
|
return {
|
||||||
"total": total,
|
"total": total,
|
||||||
"closed": closed,
|
"closed": closed,
|
||||||
@@ -491,6 +529,16 @@ def trade_stats(
|
|||||||
reverse=True,
|
reverse=True,
|
||||||
),
|
),
|
||||||
"by_side": [_finalize_group(g) for g in by_side.values()],
|
"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)
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -699,7 +699,13 @@ def register_routes(app: Flask) -> None:
|
|||||||
charts[tf] = database.get_bars(
|
charts[tf] = database.get_bars(
|
||||||
conn, trade["symbol"], tf, float(center) - window, float(center) + window
|
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/<int:tid>/annotate")
|
@app.post("/records/<int:tid>/annotate")
|
||||||
@login_required
|
@login_required
|
||||||
@@ -707,11 +713,17 @@ def register_routes(app: Flask) -> None:
|
|||||||
u = current_user()
|
u = current_user()
|
||||||
entry_style = (request.form.get("entry_style") or "").strip()
|
entry_style = (request.form.get("entry_style") or "").strip()
|
||||||
note = (request.form.get("review_note") or "").strip()
|
note = (request.form.get("review_note") or "").strip()
|
||||||
|
allowed = set(database.ENTRY_STYLES)
|
||||||
with database.get_db() as conn:
|
with database.get_db() as conn:
|
||||||
trade = database.get_trade(conn, tid)
|
trade = database.get_trade(conn, tid)
|
||||||
if not trade or not database.user_can_access_account(u, trade.get("account_id")):
|
if not trade or not database.user_can_access_account(u, trade.get("account_id")):
|
||||||
flash("无权标注该记录", "error")
|
flash("无权标注该记录", "error")
|
||||||
return redirect(url_for("records_page"))
|
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(
|
conn.execute(
|
||||||
"UPDATE trades SET entry_style=?, review_note=? WHERE id=?",
|
"UPDATE trades SET entry_style=?, review_note=? WHERE id=?",
|
||||||
(entry_style, note, tid),
|
(entry_style, note, tid),
|
||||||
|
|||||||
@@ -27,11 +27,29 @@
|
|||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label>入场形式</label>
|
<label>入场形式</label>
|
||||||
<select name="entry_style">
|
<select name="entry_style">
|
||||||
{% set styles = ['', 'M15关键位+M1强吞没', '回撤接力', '突破延续', '逆势试错', '其它'] %}
|
<option value="" {% if not trade.entry_style %}selected{% endif %}>(未选)</option>
|
||||||
{% for s in styles %}
|
<optgroup label="顺势">
|
||||||
<option value="{{ s }}" {% if trade.entry_style==s %}selected{% endif %}>{{ s or '(未选)' }}</option>
|
{% for p in entry_patterns %}
|
||||||
{% endfor %}
|
{% set s = '顺势 · ' ~ p %}
|
||||||
|
<option value="{{ s }}" {% if trade.entry_style==s %}selected{% endif %}>{{ p }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="反转">
|
||||||
|
{% for p in entry_patterns %}
|
||||||
|
{% set s = '反转 · ' ~ p %}
|
||||||
|
<option value="{{ s }}" {% if trade.entry_style==s %}selected{% endif %}>{{ p }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="其他">
|
||||||
|
<option value="其他 · 乱作" {% if trade.entry_style=='其他 · 乱作' %}selected{% endif %}>乱作</option>
|
||||||
|
</optgroup>
|
||||||
|
{% if trade.entry_style and trade.entry_style not in entry_styles %}
|
||||||
|
<optgroup label="历史标注">
|
||||||
|
<option value="{{ trade.entry_style }}" selected>{{ trade.entry_style }}(旧)</option>
|
||||||
|
</optgroup>
|
||||||
|
{% endif %}
|
||||||
</select>
|
</select>
|
||||||
|
<p class="muted" style="margin:8px 0 0">顺势/反转均含:窄幅区间突破、宽幅区间假突破、波段高低点假突破;对不上的选「乱作」。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label>复盘备注</label>
|
<label>复盘备注</label>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% block title %}统计看板 · {{ app_name }}{% endblock %}
|
{% block title %}统计看板 · {{ app_name }}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2 style="margin:0 0 6px">统计看板</h2>
|
<h2 style="margin:0 0 6px">统计看板</h2>
|
||||||
<p class="muted" style="margin:0 0 10px">程序汇总成交数据;入场形式分类确定后再叠加维度。</p>
|
<p class="muted" style="margin:0 0 10px">程序汇总:顺势/反转 × 三种结构手法,其他=乱作。</p>
|
||||||
|
|
||||||
<div style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap">
|
<div style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap">
|
||||||
<a class="btn secondary" href="{{ url_for('records_page', machine_id=selected_machine_id) if selected_machine_id else url_for('records_page') }}">交易列表</a>
|
<a class="btn secondary" href="{{ url_for('records_page', machine_id=selected_machine_id) if selected_machine_id else url_for('records_page') }}">交易列表</a>
|
||||||
@@ -65,7 +65,60 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="muted">入场形式标注</div>
|
<div class="muted">入场形式标注</div>
|
||||||
<div class="stat" style="font-size:22px">{{ stats.annotated }} <span class="muted" style="font-size:14px">/ {{ stats.total }}</span></div>
|
<div class="stat" style="font-size:22px">{{ stats.annotated }} <span class="muted" style="font-size:14px">/ {{ stats.total }}</span></div>
|
||||||
<div class="muted" style="margin-top:6px">未标注 {{ stats.unannotated }}(分类确定后再细拆)</div>
|
<div class="muted" style="margin-top:6px">未标注 {{ stats.unannotated }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid cols-2" style="margin-top:14px">
|
||||||
|
<div class="card">
|
||||||
|
<h3>按大类(顺势 / 反转 / 其他)</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>大类</th><th>笔数</th><th>胜率</th><th>盈亏</th><th>平均持仓</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for g in stats.by_category %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ g.label }}</td>
|
||||||
|
<td>{{ g.count }}</td>
|
||||||
|
<td>{{ '%.1f'|format(g.win_rate) }}%</td>
|
||||||
|
<td style="color:{% if g.profit>0 %}var(--green){% elif g.profit<0 %}var(--red){% else %}inherit{% endif %}">
|
||||||
|
{{ '%.2f'|format(g.profit) }}
|
||||||
|
</td>
|
||||||
|
<td class="muted">{{ g.avg_hold }}</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr><td colspan="5" class="muted">暂无数据</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>按入场形式(细项)</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>形式</th><th>笔数</th><th>胜率</th><th>盈亏</th><th>平均持仓</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for g in stats.by_style %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ g.label }}</td>
|
||||||
|
<td>{{ g.count }}</td>
|
||||||
|
<td>{{ '%.1f'|format(g.win_rate) }}%</td>
|
||||||
|
<td style="color:{% if g.profit>0 %}var(--green){% elif g.profit<0 %}var(--red){% else %}inherit{% endif %}">
|
||||||
|
{{ '%.2f'|format(g.profit) }}
|
||||||
|
</td>
|
||||||
|
<td class="muted">{{ g.avg_hold }}</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr><td colspan="5" class="muted">暂无数据</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user