feat: strategy trade snapshots, DCA detail, and hub trend layout
Persist ended trend pullback and roll group snapshots to a unified records page; show replenishment tiers on instance and hub cards with horizontal single-position layout. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6008,12 +6008,12 @@ def render_main_page(page="trade"):
|
||||
f"【阻力/支撑】填上/下沿,5m 收盘突破任一侧即提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分),不选方向、不自动开仓"
|
||||
)
|
||||
strategy_extra = {}
|
||||
if page in ("strategy", "strategy_trend", "strategy_roll"):
|
||||
from strategy_ui import strategy_page_template_vars
|
||||
if page in ("strategy", "strategy_trend", "strategy_roll", "strategy_records"):
|
||||
from strategy_ui import strategy_render_extras
|
||||
|
||||
strategy_extra = strategy_page_template_vars(
|
||||
strategy_extra = strategy_render_extras(
|
||||
conn,
|
||||
"strategy",
|
||||
page,
|
||||
default_risk_percent=float(RISK_PERCENT),
|
||||
request_obj=request,
|
||||
trend_cfg=app.extensions.get("strategy_trend_cfg"),
|
||||
|
||||
@@ -259,6 +259,7 @@
|
||||
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
|
||||
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">实盘下单</a>
|
||||
<a href="/strategy" class="{% if page in ('strategy', 'strategy_trend', 'strategy_roll') %}active{% endif %}">策略交易</a>
|
||||
<a href="/strategy/records" class="{% if page == 'strategy_records' %}active{% endif %}">策略交易记录</a>
|
||||
<a href="/records" class="{% if page == 'records' %}active{% endif %}">交易记录与复盘</a>
|
||||
<a href="/stats" class="{% if page == 'stats' %}active{% endif %}">统计分析</a>
|
||||
</div>
|
||||
@@ -594,6 +595,8 @@
|
||||
</div>
|
||||
{% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %}
|
||||
{% include 'strategy_trading_page.html' %}
|
||||
{% elif page == 'strategy_records' %}
|
||||
{% include 'strategy_records_page.html' %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
@@ -5964,12 +5964,12 @@ def render_main_page(page="trade"):
|
||||
f"【阻力/支撑】填上/下沿,5m 收盘突破任一侧即提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分),不选方向、不自动开仓"
|
||||
)
|
||||
strategy_extra = {}
|
||||
if page in ("strategy", "strategy_trend", "strategy_roll"):
|
||||
from strategy_ui import strategy_page_template_vars
|
||||
if page in ("strategy", "strategy_trend", "strategy_roll", "strategy_records"):
|
||||
from strategy_ui import strategy_render_extras
|
||||
|
||||
strategy_extra = strategy_page_template_vars(
|
||||
strategy_extra = strategy_render_extras(
|
||||
conn,
|
||||
"strategy",
|
||||
page,
|
||||
default_risk_percent=float(RISK_PERCENT),
|
||||
request_obj=request,
|
||||
trend_cfg=app.extensions.get("strategy_trend_cfg"),
|
||||
|
||||
@@ -259,6 +259,7 @@
|
||||
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
|
||||
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">实盘下单</a>
|
||||
<a href="/strategy" class="{% if page in ('strategy', 'strategy_trend', 'strategy_roll') %}active{% endif %}">策略交易</a>
|
||||
<a href="/strategy/records" class="{% if page == 'strategy_records' %}active{% endif %}">策略交易记录</a>
|
||||
<a href="/records" class="{% if page == 'records' %}active{% endif %}">交易记录与复盘</a>
|
||||
<a href="/stats" class="{% if page == 'stats' %}active{% endif %}">统计分析</a>
|
||||
</div>
|
||||
@@ -594,6 +595,8 @@
|
||||
</div>
|
||||
{% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %}
|
||||
{% include 'strategy_trading_page.html' %}
|
||||
{% elif page == 'strategy_records' %}
|
||||
{% include 'strategy_records_page.html' %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
@@ -3824,7 +3824,9 @@ def enrich_active_trend_plan_row(row):
|
||||
d["floating_mark"] = float(m["mark_price"])
|
||||
else:
|
||||
d["floating_mark"] = None
|
||||
return d
|
||||
from strategy_snapshot_lib import attach_trend_dca_levels
|
||||
|
||||
return attach_trend_dca_levels(d)
|
||||
|
||||
|
||||
def opened_at_str_to_ms(opened_at_str):
|
||||
@@ -4602,6 +4604,25 @@ def _trend_finalize_plan(conn, row, result_label, exit_price, closed_at=None):
|
||||
if not getattr(cur, "rowcount", 0):
|
||||
return
|
||||
conn.commit()
|
||||
try:
|
||||
cfg = app.extensions.get("strategy_trend_cfg") or {}
|
||||
closed = conn.execute(
|
||||
"SELECT * FROM trend_pullback_plans WHERE id=?", (plan_id,)
|
||||
).fetchone()
|
||||
if closed and cfg:
|
||||
from strategy_snapshot_lib import save_trend_plan_snapshot
|
||||
|
||||
save_trend_plan_snapshot(
|
||||
cfg,
|
||||
conn,
|
||||
closed,
|
||||
result_label=result_label,
|
||||
exit_price=float(exit_price),
|
||||
pnl_amount=float(pnl_amount) if pnl_amount is not None else None,
|
||||
)
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
if _trend_plan_trade_exists(conn, plan_id):
|
||||
return
|
||||
session_date = row["session_date"] or get_trading_day()
|
||||
@@ -5457,7 +5478,11 @@ def render_main_page(page="trade"):
|
||||
elif pr:
|
||||
trend_preview_expired = True
|
||||
strategy_extra = {}
|
||||
if page in ("strategy", "strategy_trend", "strategy_roll"):
|
||||
if page == "strategy_records":
|
||||
from strategy_ui import strategy_render_extras
|
||||
|
||||
strategy_extra = strategy_render_extras(conn, page)
|
||||
elif page in ("strategy", "strategy_trend", "strategy_roll"):
|
||||
from strategy_ui import fetch_roll_page_data
|
||||
|
||||
strategy_extra = fetch_roll_page_data(
|
||||
|
||||
@@ -248,6 +248,7 @@
|
||||
<div class="top-nav">
|
||||
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">交易执行</a>
|
||||
<a href="/strategy" class="{% if page in ('strategy', 'strategy_trend', 'strategy_roll') %}active{% endif %}">策略交易</a>
|
||||
<a href="/strategy/records" class="{% if page == 'strategy_records' %}active{% endif %}">策略交易记录</a>
|
||||
<a href="/records" class="{% if page == 'records' %}active{% endif %}">交易记录与复盘</a>
|
||||
<a href="/stats" class="{% if page == 'stats' %}active{% endif %}">统计分析</a>
|
||||
</div>
|
||||
@@ -422,6 +423,8 @@
|
||||
{% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %}
|
||||
{% set can_trade_trend = can_trade %}
|
||||
{% include 'strategy_trading_page.html' %}
|
||||
{% elif page == 'strategy_records' %}
|
||||
{% include 'strategy_records_page.html' %}
|
||||
{% endif %}
|
||||
|
||||
{% if page == 'records' %}
|
||||
|
||||
@@ -5608,12 +5608,12 @@ def render_main_page(page="trade"):
|
||||
f"【阻力/支撑】填上/下沿,5m 收盘突破任一侧即提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分),不选方向、不自动开仓"
|
||||
)
|
||||
strategy_extra = {}
|
||||
if page in ("strategy", "strategy_trend", "strategy_roll"):
|
||||
from strategy_ui import strategy_page_template_vars
|
||||
if page in ("strategy", "strategy_trend", "strategy_roll", "strategy_records"):
|
||||
from strategy_ui import strategy_render_extras
|
||||
|
||||
strategy_extra = strategy_page_template_vars(
|
||||
strategy_extra = strategy_render_extras(
|
||||
conn,
|
||||
"strategy",
|
||||
page,
|
||||
default_risk_percent=float(RISK_PERCENT),
|
||||
request_obj=request,
|
||||
trend_cfg=app.extensions.get("strategy_trend_cfg"),
|
||||
|
||||
@@ -259,6 +259,7 @@
|
||||
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
|
||||
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">实盘下单</a>
|
||||
<a href="/strategy" class="{% if page in ('strategy', 'strategy_trend', 'strategy_roll') %}active{% endif %}">策略交易</a>
|
||||
<a href="/strategy/records" class="{% if page == 'strategy_records' %}active{% endif %}">策略交易记录</a>
|
||||
<a href="/records" class="{% if page == 'records' %}active{% endif %}">交易记录与复盘</a>
|
||||
<a href="/stats" class="{% if page == 'stats' %}active{% endif %}">统计分析</a>
|
||||
</div>
|
||||
@@ -603,6 +604,8 @@
|
||||
</div>
|
||||
{% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %}
|
||||
{% include 'strategy_trading_page.html' %}
|
||||
{% elif page == 'strategy_records' %}
|
||||
{% include 'strategy_records_page.html' %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
@@ -1375,15 +1375,84 @@ body.market-chart-fs-open {
|
||||
color: #8fc8ff;
|
||||
}
|
||||
|
||||
.exchange-fullscreen .hub-trend-plan-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 14px;
|
||||
align-items: stretch;
|
||||
.hub-trend-plan-card--horizontal {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 14px 18px;
|
||||
align-items: flex-start;
|
||||
background: linear-gradient(145deg, rgba(12, 18, 32, 0.92), rgba(8, 12, 22, 0.88));
|
||||
border-color: rgba(0, 212, 255, 0.22);
|
||||
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.exchange-fullscreen .hub-trend-plan-card {
|
||||
height: 100%;
|
||||
.hub-trend-plan-body {
|
||||
flex: 1 1 320px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hub-trend-plan-side {
|
||||
flex: 1 1 220px;
|
||||
min-width: 200px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.hub-trend-dca-block {
|
||||
padding: 8px 10px;
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.hub-trend-dca-title {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.hub-trend-dca-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.hub-trend-dca-table th,
|
||||
.hub-trend-dca-table td {
|
||||
padding: 4px 6px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.hub-trend-dca-table th {
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hub-trend-dca-table .dca-done {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.hub-trend-dca-table .dca-pending {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.exchange-fullscreen .hub-trend-plan-list {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.exchange-fullscreen .hub-trend-plan-card--horizontal {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.exchange-fullscreen .hub-trend-plan-metrics-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 10px 16px;
|
||||
}
|
||||
|
||||
.exchange-fullscreen .hub-trend-plan-grid {
|
||||
flex: 1 1 280px;
|
||||
}
|
||||
|
||||
/* 顺势加仓 */
|
||||
|
||||
@@ -1513,6 +1513,37 @@
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderTrendDcaTable(t, tickMap) {
|
||||
const levels = Array.isArray(t.dca_levels) ? t.dca_levels : [];
|
||||
if (!levels.length) return "";
|
||||
const sym = t.exchange_symbol || t.symbol || "";
|
||||
const rows = levels
|
||||
.map((lv) => {
|
||||
const price =
|
||||
lv.price != null && lv.price !== ""
|
||||
? fmtSymbolPrice(lv.price, sym, tickMap)
|
||||
: "—";
|
||||
const amt =
|
||||
lv.contracts != null && lv.contracts !== "" ? esc(String(lv.contracts)) : "—";
|
||||
const stCls = lv.status === "done" ? "dca-done" : "dca-pending";
|
||||
const label = lv.status_label || (lv.status === "done" ? "已补仓" : "待补仓");
|
||||
return `<tr>
|
||||
<td>${esc(lv.label || lv.leg_key || "—")}</td>
|
||||
<td>${esc(price)}</td>
|
||||
<td>${amt}</td>
|
||||
<td class="${stCls}">${esc(label)}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
return `<div class="hub-trend-dca-block">
|
||||
<div class="hub-trend-dca-title">补仓计划明细</div>
|
||||
<table class="hub-trend-dca-table">
|
||||
<thead><tr><th>档位</th><th>触发价</th><th>张数</th><th>状态</th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderTrendPlanCard(t, tickMap, pos) {
|
||||
const sym = t.exchange_symbol || t.symbol || "";
|
||||
const side = (t.direction || "long").toLowerCase();
|
||||
@@ -1560,26 +1591,32 @@
|
||||
<span>计划基数: ${baseTxt}</span>
|
||||
<span>仓位占比: ${ratioTxt}</span>
|
||||
</div>`;
|
||||
return `<div class="hub-trend-plan-card hub-pos-card">
|
||||
<div class="hub-trend-plan-head">
|
||||
<span class="hub-trend-plan-title">#${esc(t.id)} ${esc(sym)} ${renderDirectionHtml(t.direction)}</span>
|
||||
<span class="hub-trend-plan-status">${esc(t.status || "active")}</span>
|
||||
const dcaHtml = renderTrendDcaTable(t, tickMap);
|
||||
return `<div class="hub-trend-plan-card hub-pos-card hub-trend-plan-card--horizontal">
|
||||
<div class="hub-trend-plan-body">
|
||||
<div class="hub-trend-plan-head">
|
||||
<span class="hub-trend-plan-title">#${esc(t.id)} ${esc(sym)} ${renderDirectionHtml(t.direction)}</span>
|
||||
<span class="hub-trend-plan-status">${esc(t.status || "active")}</span>
|
||||
</div>
|
||||
<div class="hub-trend-plan-meta">
|
||||
<span>来源: 趋势回调计划</span>
|
||||
<span>风险: ${riskTxt}</span>
|
||||
<span>${esc(trendAddZoneLabel(t.direction))} ${esc(addZone)}</span>
|
||||
<span>已补仓 <strong>${legsTxt}</strong></span>
|
||||
</div>
|
||||
<div class="hub-trend-plan-metrics-row">
|
||||
<div class="pos-grid hub-trend-plan-grid">
|
||||
<div class="pos-cell"><span class="pos-label">均价</span><span class="pos-value">${esc(avg)}</span></div>
|
||||
<div class="pos-cell"><span class="pos-label">止损</span><span class="pos-value">${esc(sl)}</span></div>
|
||||
<div class="pos-cell"><span class="pos-label">止盈</span><span class="pos-value pos-tp-program">程序监控 · ${esc(tp)}</span></div>
|
||||
<div class="pos-cell"><span class="pos-label">盈亏比</span><span class="pos-value">${esc(rrTxt)}</span></div>
|
||||
<div class="pos-cell"><span class="pos-label">标记价</span><span class="pos-value">${esc(mark)}</span></div>
|
||||
<div class="pos-cell"><span class="pos-label">浮盈亏</span>${pnlInner}</div>
|
||||
</div>
|
||||
${footHtml}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hub-trend-plan-meta">
|
||||
<span>来源: 趋势回调计划</span>
|
||||
<span>风险: ${riskTxt}</span>
|
||||
<span>${esc(trendAddZoneLabel(t.direction))} ${esc(addZone)}</span>
|
||||
<span>已补仓 <strong>${legsTxt}</strong></span>
|
||||
</div>
|
||||
<div class="pos-grid hub-trend-plan-grid">
|
||||
<div class="pos-cell"><span class="pos-label">均价</span><span class="pos-value">${esc(avg)}</span></div>
|
||||
<div class="pos-cell"><span class="pos-label">止损</span><span class="pos-value">${esc(sl)}</span></div>
|
||||
<div class="pos-cell"><span class="pos-label">止盈</span><span class="pos-value pos-tp-program">程序监控 · ${esc(tp)}</span></div>
|
||||
<div class="pos-cell"><span class="pos-label">盈亏比</span><span class="pos-value">${esc(rrTxt)}</span></div>
|
||||
<div class="pos-cell"><span class="pos-label">标记价</span><span class="pos-value">${esc(mark)}</span></div>
|
||||
<div class="pos-cell"><span class="pos-label">浮盈亏</span>${pnlInner}</div>
|
||||
</div>
|
||||
${footHtml}
|
||||
${dcaHtml ? `<div class="hub-trend-plan-side">${dcaHtml}</div>` : ""}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
||||
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
||||
<link rel="stylesheet" href="/assets/app.css?v=20260604-hub-trend-metrics" />
|
||||
<link rel="stylesheet" href="/assets/app.css?v=20260604-hub-trend-dca" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-bg" aria-hidden="true"></div>
|
||||
@@ -235,6 +235,6 @@
|
||||
<div id="toast"></div>
|
||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script src="/assets/chart.js?v=20260604-hub-trend-plan"></script>
|
||||
<script src="/assets/app.js?v=20260604-hub-trend-metrics"></script>
|
||||
<script src="/assets/app.js?v=20260604-hub-trend-dca"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -133,11 +133,14 @@ CREATE TABLE IF NOT EXISTS trend_pullback_preview_snapshots (
|
||||
|
||||
|
||||
def init_strategy_tables(conn) -> None:
|
||||
from strategy_snapshot_lib import init_strategy_snapshot_table
|
||||
|
||||
conn.execute(ROLL_GROUPS_SQL)
|
||||
conn.execute(ROLL_LEGS_SQL)
|
||||
conn.execute(TREND_PLANS_SQL)
|
||||
conn.execute(TREND_PREVIEWS_SQL)
|
||||
conn.execute(TREND_PREVIEW_SNAPSHOTS_SQL)
|
||||
init_strategy_snapshot_table(conn)
|
||||
for ddl in (
|
||||
"ALTER TABLE trend_pullback_plans ADD COLUMN leg_amounts_json TEXT",
|
||||
"ALTER TABLE trend_pullback_plans ADD COLUMN initial_stop_loss REAL",
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""策略交易记录页:已结束趋势 / 顺势加仓快照(四所统一)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from flask import flash, redirect, url_for
|
||||
|
||||
from strategy_snapshot_lib import list_strategy_snapshots
|
||||
|
||||
|
||||
def load_strategy_records_page(conn, *, limit: int = 200) -> dict[str, Any]:
|
||||
snaps = list_strategy_snapshots(conn, limit=limit)
|
||||
return {"strategy_snapshots": snaps, "strategy_records_limit": limit}
|
||||
|
||||
|
||||
def register_strategy_records(app, cfg: dict[str, Any]) -> None:
|
||||
login_required = cfg["login_required"]
|
||||
get_db = cfg["get_db"]
|
||||
|
||||
def _lr(f):
|
||||
return login_required(f)
|
||||
|
||||
@_lr
|
||||
@app.route("/strategy/records")
|
||||
def strategy_records_page():
|
||||
m = cfg.get("app_module")
|
||||
fn = getattr(m, "render_main_page", None)
|
||||
if not callable(fn):
|
||||
flash("render_main_page 未配置")
|
||||
return redirect(url_for("strategy_trading_page"))
|
||||
return fn("strategy_records")
|
||||
|
||||
@_lr
|
||||
@app.route("/strategy/records/<int:snap_id>")
|
||||
def strategy_records_detail(snap_id: int):
|
||||
conn = get_db()
|
||||
row = conn.execute(
|
||||
"SELECT * FROM strategy_trade_snapshots WHERE id=?",
|
||||
(int(snap_id),),
|
||||
).fetchone()
|
||||
conn.close()
|
||||
if not row:
|
||||
flash("未找到该策略快照")
|
||||
return redirect(url_for("strategy_records_page"))
|
||||
try:
|
||||
snap = json.loads(row["snapshot_json"] or "{}")
|
||||
except Exception:
|
||||
snap = {}
|
||||
dca = snap.get("dca_levels") or []
|
||||
flash(
|
||||
f"快照 #{snap_id} {row['strategy_type']} {row['symbol']} "
|
||||
f"{row['result_label']} · 补仓档 {len(dca)} 项(详情见列表页)"
|
||||
)
|
||||
return redirect(url_for("strategy_records_page"))
|
||||
@@ -20,6 +20,9 @@ def install_strategy_trading(app: Flask, repo_root: str, app_module: Any = None,
|
||||
attach_strategy_templates(app, repo_root)
|
||||
cfg = build_strategy_config(app_module, **build_kw)
|
||||
register_strategy_trading(app, cfg)
|
||||
from strategy_records_register import register_strategy_records
|
||||
|
||||
register_strategy_records(app, cfg)
|
||||
app.extensions["strategy_roll_cfg"] = cfg
|
||||
|
||||
|
||||
|
||||
@@ -76,6 +76,12 @@ def _close_roll_group(
|
||||
"UPDATE roll_groups SET status='closed', updated_at=? WHERE id=? AND status='active'",
|
||||
(_now(cfg), gid),
|
||||
)
|
||||
try:
|
||||
from strategy_snapshot_lib import save_roll_group_snapshot
|
||||
|
||||
save_roll_group_snapshot(cfg, conn, group, result_label="结束")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _reconcile_roll_groups(conn, cfg: dict) -> None:
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
"""策略结束快照:趋势回调 / 顺势加仓(四所共用)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
STRATEGY_TREND = "trend_pullback"
|
||||
STRATEGY_ROLL = "roll"
|
||||
|
||||
STRATEGY_SNAPSHOTS_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS strategy_trade_snapshots (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
strategy_type TEXT NOT NULL,
|
||||
source_id INTEGER,
|
||||
symbol TEXT,
|
||||
exchange_symbol TEXT,
|
||||
direction TEXT,
|
||||
result_label TEXT,
|
||||
status_at_close TEXT,
|
||||
opened_at TEXT,
|
||||
closed_at TEXT,
|
||||
pnl_amount REAL,
|
||||
snapshot_json TEXT NOT NULL,
|
||||
created_at TEXT
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def init_strategy_snapshot_table(conn) -> None:
|
||||
conn.execute(STRATEGY_SNAPSHOTS_SQL)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_strategy_snapshots_closed "
|
||||
"ON strategy_trade_snapshots(closed_at DESC)"
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_strategy_snapshots_type "
|
||||
"ON strategy_trade_snapshots(strategy_type, source_id)"
|
||||
)
|
||||
|
||||
|
||||
def _row_dict(row) -> dict:
|
||||
if row is None:
|
||||
return {}
|
||||
try:
|
||||
return dict(row)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _json_dumps(obj: Any) -> str:
|
||||
return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
|
||||
def build_trend_dca_levels(plan: dict) -> list[dict]:
|
||||
"""首仓 + 补仓档位列表(供策略页 / 中控)。"""
|
||||
out: list[dict] = []
|
||||
p = plan or {}
|
||||
try:
|
||||
legs_done = int(p.get("legs_done") or 0)
|
||||
except (TypeError, ValueError):
|
||||
legs_done = 0
|
||||
try:
|
||||
dca_legs = int(p.get("dca_legs") or 0)
|
||||
except (TypeError, ValueError):
|
||||
dca_legs = 0
|
||||
first_done = int(p.get("first_order_done") or 0) != 0
|
||||
try:
|
||||
grid = json.loads(p.get("grid_prices_json") or "[]")
|
||||
if not isinstance(grid, list):
|
||||
grid = []
|
||||
except Exception:
|
||||
grid = []
|
||||
try:
|
||||
leg_amounts = json.loads(p.get("leg_amounts_json") or "[]")
|
||||
if not isinstance(leg_amounts, list):
|
||||
leg_amounts = []
|
||||
except Exception:
|
||||
leg_amounts = []
|
||||
|
||||
out.append(
|
||||
{
|
||||
"i": 0,
|
||||
"leg_key": "first",
|
||||
"label": "首仓",
|
||||
"price": None,
|
||||
"contracts": p.get("first_order_amount"),
|
||||
"status": "done" if first_done else "pending",
|
||||
"status_label": "已开仓" if first_done else "待开仓",
|
||||
}
|
||||
)
|
||||
n = max(len(grid), len(leg_amounts), dca_legs)
|
||||
for idx in range(n):
|
||||
leg_i = idx + 1
|
||||
price = grid[idx] if idx < len(grid) else None
|
||||
contracts = leg_amounts[idx] if idx < len(leg_amounts) else None
|
||||
done = leg_i <= legs_done
|
||||
out.append(
|
||||
{
|
||||
"i": leg_i,
|
||||
"leg_key": f"dca_{leg_i}",
|
||||
"label": f"补仓{leg_i}",
|
||||
"price": price,
|
||||
"contracts": contracts,
|
||||
"status": "done" if done else "pending",
|
||||
"status_label": "已补仓" if done else "待补仓",
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def attach_trend_dca_levels(plan: dict) -> dict:
|
||||
d = dict(plan or {})
|
||||
d["dca_levels"] = build_trend_dca_levels(d)
|
||||
return d
|
||||
|
||||
|
||||
def save_trend_plan_snapshot(
|
||||
cfg: dict,
|
||||
conn,
|
||||
plan_row: Any,
|
||||
*,
|
||||
result_label: str,
|
||||
exit_price: float | None = None,
|
||||
pnl_amount: float | None = None,
|
||||
) -> None:
|
||||
init_strategy_snapshot_table(conn)
|
||||
row = _row_dict(plan_row)
|
||||
plan_id = int(row.get("id") or 0)
|
||||
if plan_id <= 0:
|
||||
return
|
||||
m = cfg.get("app_module")
|
||||
closed_at = (
|
||||
m.app_now_str()
|
||||
if m is not None and hasattr(m, "app_now_str")
|
||||
else datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
payload = attach_trend_dca_levels(row)
|
||||
payload["result_label"] = result_label
|
||||
payload["exit_price"] = exit_price
|
||||
payload["pnl_amount"] = pnl_amount
|
||||
payload["status_at_close"] = row.get("status")
|
||||
conn.execute(
|
||||
"""INSERT INTO strategy_trade_snapshots (
|
||||
strategy_type, source_id, symbol, exchange_symbol, direction,
|
||||
result_label, status_at_close, opened_at, closed_at, pnl_amount, snapshot_json, created_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
STRATEGY_TREND,
|
||||
plan_id,
|
||||
row.get("symbol"),
|
||||
row.get("exchange_symbol"),
|
||||
row.get("direction"),
|
||||
result_label,
|
||||
row.get("status"),
|
||||
row.get("opened_at"),
|
||||
closed_at,
|
||||
pnl_amount,
|
||||
_json_dumps(payload),
|
||||
closed_at,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def save_roll_group_snapshot(
|
||||
cfg: dict,
|
||||
conn,
|
||||
group: dict,
|
||||
*,
|
||||
result_label: str = "结束",
|
||||
pnl_amount: float | None = None,
|
||||
) -> None:
|
||||
init_strategy_snapshot_table(conn)
|
||||
g = dict(group or {})
|
||||
gid = int(g.get("id") or 0)
|
||||
if gid <= 0:
|
||||
return
|
||||
legs = []
|
||||
for leg in conn.execute(
|
||||
"SELECT * FROM roll_legs WHERE roll_group_id=? ORDER BY leg_index ASC, id ASC",
|
||||
(gid,),
|
||||
).fetchall():
|
||||
ld = _row_dict(leg)
|
||||
try:
|
||||
from strategy_roll_monitor_lib import roll_leg_status_label
|
||||
|
||||
ld["status_label"] = roll_leg_status_label(ld.get("status"))
|
||||
except Exception:
|
||||
ld["status_label"] = ld.get("status") or ""
|
||||
legs.append(ld)
|
||||
m = cfg.get("app_module")
|
||||
closed_at = (
|
||||
m.app_now_str()
|
||||
if m is not None and hasattr(m, "app_now_str")
|
||||
else datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
payload = {
|
||||
"group": g,
|
||||
"legs": legs,
|
||||
"result_label": result_label,
|
||||
"pnl_amount": pnl_amount,
|
||||
}
|
||||
conn.execute(
|
||||
"""INSERT INTO strategy_trade_snapshots (
|
||||
strategy_type, source_id, symbol, exchange_symbol, direction,
|
||||
result_label, status_at_close, opened_at, closed_at, pnl_amount, snapshot_json, created_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
STRATEGY_ROLL,
|
||||
gid,
|
||||
g.get("symbol"),
|
||||
g.get("exchange_symbol"),
|
||||
g.get("direction"),
|
||||
result_label,
|
||||
g.get("status"),
|
||||
g.get("created_at"),
|
||||
closed_at,
|
||||
pnl_amount,
|
||||
_json_dumps(payload),
|
||||
closed_at,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def list_strategy_snapshots(conn, *, limit: int = 200) -> list[dict]:
|
||||
init_strategy_snapshot_table(conn)
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM strategy_trade_snapshots ORDER BY id DESC LIMIT ?",
|
||||
(max(1, min(int(limit), 500)),),
|
||||
).fetchall()
|
||||
out = []
|
||||
for r in rows:
|
||||
d = _row_dict(r)
|
||||
try:
|
||||
d["snapshot"] = json.loads(d.get("snapshot_json") or "{}")
|
||||
except Exception:
|
||||
d["snapshot"] = {}
|
||||
st = (d.get("strategy_type") or "").strip()
|
||||
d["strategy_label"] = "趋势回调" if st == STRATEGY_TREND else "顺势加仓"
|
||||
out.append(d)
|
||||
return out
|
||||
@@ -0,0 +1,89 @@
|
||||
<style>
|
||||
.strategy-records-page{padding:4px 0 16px}
|
||||
.strategy-records-page h2{margin:0 0 10px;color:#dbe4ff}
|
||||
.strategy-records-tip{font-size:.78rem;color:#8892b0;line-height:1.55;margin-bottom:14px}
|
||||
.strategy-records-table{width:100%;border-collapse:collapse;font-size:.82rem}
|
||||
.strategy-records-table th,.strategy-records-table td{padding:8px 10px;border-bottom:1px solid #2a3150;text-align:left}
|
||||
.strategy-records-table th{color:#8b95b8;font-weight:600;font-size:.74rem}
|
||||
.strategy-records-table tr:hover td{background:rgba(42,63,108,.25)}
|
||||
.strategy-records-table .tag-trend{color:#6ab8ff}
|
||||
.strategy-records-table .tag-roll{color:#ffb020}
|
||||
.strategy-records-detail{margin-top:8px;padding:10px 12px;background:#0f1424;border:1px solid #2a3150;border-radius:8px;font-size:.76rem;color:#cfd3ef;line-height:1.5}
|
||||
.strategy-dca-mini{width:100%;margin-top:6px;border-collapse:collapse;font-size:.72rem}
|
||||
.strategy-dca-mini th,.strategy-dca-mini td{padding:4px 8px;border-bottom:1px solid #243050}
|
||||
.strategy-dca-mini .st-done{color:#4cd97f}
|
||||
.strategy-dca-mini .st-pending{color:#8892b0}
|
||||
.strategy-snap-pnl.pos{color:#4cd97f}
|
||||
.strategy-snap-pnl.neg{color:#ff6666}
|
||||
</style>
|
||||
<div class="strategy-records-page card full">
|
||||
<h2>策略交易记录</h2>
|
||||
<p class="strategy-records-tip">
|
||||
已结束的趋势回调计划与顺势加仓组会在此留存快照(含补仓档位明细)。保本移交、手动结束、止盈止损均会写入。
|
||||
</p>
|
||||
{% if strategy_snapshots %}
|
||||
<div class="table-wrap">
|
||||
<table class="strategy-records-table">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>策略</th>
|
||||
<th>品种</th>
|
||||
<th>方向</th>
|
||||
<th>结果</th>
|
||||
<th>盈亏U</th>
|
||||
<th>结束时间</th>
|
||||
<th>补仓明细</th>
|
||||
</tr>
|
||||
{% for s in strategy_snapshots %}
|
||||
{% set snap = s.snapshot or {} %}
|
||||
{% set dca = snap.dca_levels if snap.dca_levels is defined else [] %}
|
||||
{% set pnl = s.pnl_amount if s.pnl_amount is not none else snap.pnl_amount %}
|
||||
<tr>
|
||||
<td>{{ s.id }}</td>
|
||||
<td><span class="{% if s.strategy_type == 'trend_pullback' %}tag-trend{% else %}tag-roll{% endif %}">{{ s.strategy_label }}</span></td>
|
||||
<td>{{ s.symbol or s.exchange_symbol or '—' }}</td>
|
||||
<td><span class="badge {{ 'direction-long' if s.direction == 'long' else 'direction-short' }}">{{ '做多' if s.direction == 'long' else '做空' }}</span></td>
|
||||
<td>{{ s.result_label or '—' }}</td>
|
||||
<td class="{% if pnl is not none %}{% if pnl|float > 0 %}strategy-snap-pnl pos{% elif pnl|float < 0 %}strategy-snap-pnl neg{% endif %}{% endif %}">
|
||||
{% if pnl is not none %}{{ funds_fmt(pnl) }}{% else %}—{% endif %}
|
||||
</td>
|
||||
<td>{{ (s.closed_at or '')[:19] }}</td>
|
||||
<td>
|
||||
{% if dca and dca|length %}
|
||||
<details>
|
||||
<summary style="cursor:pointer;color:#8fc8ff">{{ dca|length }} 档</summary>
|
||||
<table class="strategy-dca-mini">
|
||||
<tr><th>档位</th><th>触发价</th><th>张数</th><th>状态</th></tr>
|
||||
{% for lv in dca %}
|
||||
<tr>
|
||||
<td>{{ lv.label or lv.leg_key }}</td>
|
||||
<td>{% if lv.price is not none %}{{ price_fmt(s.symbol or s.exchange_symbol, lv.price) }}{% else %}—{% endif %}</td>
|
||||
<td>{% if lv.contracts is not none %}{{ lv.contracts }}{% else %}—{% endif %}</td>
|
||||
<td class="{% if lv.status == 'done' %}st-done{% else %}st-pending{% endif %}">{{ lv.status_label or '—' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</details>
|
||||
{% elif s.strategy_type == 'roll' and snap.legs %}
|
||||
<details>
|
||||
<summary style="cursor:pointer;color:#8fc8ff">{{ snap.legs|length }} 腿</summary>
|
||||
<table class="strategy-dca-mini">
|
||||
<tr><th>#</th><th>状态</th></tr>
|
||||
{% for leg in snap.legs %}
|
||||
<tr>
|
||||
<td>{{ leg.leg_index or loop.index }}</td>
|
||||
<td>{{ leg.status_label or leg.status }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</details>
|
||||
{% else %}—{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rule-tip" style="color:#8892b0">暂无策略结束快照。结束趋势回调计划或顺势加仓组后会自动出现在此。</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -19,6 +19,13 @@
|
||||
.plan-cell .val.pnl-neutral{color:#cfd3ef}
|
||||
.btn-close-plan{padding:7px 14px;background:#5c1e2a;color:#ffb4b4;border:none;border-radius:8px;cursor:pointer;font-size:.82rem;font-weight:600;text-decoration:none;white-space:nowrap;display:inline-block}
|
||||
.btn-close-plan:hover{filter:brightness(1.08)}
|
||||
.plan-dca-block{margin-top:12px;padding-top:10px;border-top:1px dashed #2a3558}
|
||||
.plan-dca-title{font-size:.74rem;color:#8b95b8;margin-bottom:8px;letter-spacing:.02em}
|
||||
.plan-dca-table{width:100%;border-collapse:collapse;font-size:.76rem}
|
||||
.plan-dca-table th,.plan-dca-table td{padding:6px 8px;border-bottom:1px solid #243050;text-align:left}
|
||||
.plan-dca-table th{color:#6a7598;font-weight:600}
|
||||
.plan-dca-table .st-done{color:#4cd97f}
|
||||
.plan-dca-table .st-pending{color:#9aa3c4}
|
||||
@media (max-width:720px){
|
||||
.plan-card-grid{grid-template-columns:1fr}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
| <span class="accent">{{ trend_add_zone_label(t.direction) }} {{ price_fmt(sym, t.add_upper) }}</span>
|
||||
| 已补仓 <strong>{{ t.legs_done }}/{{ t.dca_legs }}</strong>
|
||||
</div>
|
||||
<div class="plan-card-grid">
|
||||
<div class="plan-card-grid plan-card-grid--metrics">
|
||||
<div class="plan-cell">
|
||||
<span class="lbl">均价</span>
|
||||
<span class="val">{% if t.avg_entry_price is not none %}{{ price_fmt(sym, t.avg_entry_price) }}{% else %}—{% endif %}</span>
|
||||
@@ -155,6 +155,22 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% if t.dca_levels %}
|
||||
<div class="plan-dca-block">
|
||||
<div class="plan-dca-title">补仓计划明细</div>
|
||||
<table class="plan-dca-table">
|
||||
<tr><th>档位</th><th>触发价</th><th>张数</th><th>状态</th></tr>
|
||||
{% for lv in t.dca_levels %}
|
||||
<tr>
|
||||
<td>{{ lv.label }}</td>
|
||||
<td>{% if lv.price is not none %}{{ price_fmt(sym, lv.price) }}{% else %}—{% endif %}</td>
|
||||
<td>{% if lv.contracts is not none %}{{ amt_disp(sym, lv.contracts) }}{% else %}—{% endif %}</td>
|
||||
<td class="{% if lv.status == 'done' %}st-done{% else %}st-pending{% endif %}">{{ lv.status_label }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="plan-card-meta" style="margin-top:8px">
|
||||
<form action="{{ url_for('trend_pullback_breakeven', pid=t.id) }}" method="post" class="form-row" style="margin:0;align-items:center" onsubmit="return confirm('确认保本?将结束本趋势计划,持仓移交「下单监控」(备注趋势回调计划),并在交易所同时挂保本止损与计划止盈;后续平仓会写入交易记录。');">
|
||||
<label style="font-size:.78rem;color:#cfd3ef;display:flex;align-items:center;gap:6px">
|
||||
|
||||
@@ -434,7 +434,9 @@ def enrich_trend_plan(cfg: dict, row) -> dict:
|
||||
d["floating_mark"] = None
|
||||
else:
|
||||
d["floating_pnl"] = d["floating_mark"] = None
|
||||
return d
|
||||
from strategy_snapshot_lib import attach_trend_dca_levels
|
||||
|
||||
return attach_trend_dca_levels(d)
|
||||
|
||||
|
||||
def _weighted_avg(old_avg, old_amt, fill_px, add_amt):
|
||||
@@ -502,6 +504,24 @@ def _finalize_plan(cfg: dict, conn, row, result_label: str, exit_price: float) -
|
||||
if not getattr(cur, "rowcount", 0):
|
||||
return
|
||||
conn.commit()
|
||||
try:
|
||||
closed = conn.execute(
|
||||
"SELECT * FROM trend_pullback_plans WHERE id=?", (plan_id,)
|
||||
).fetchone()
|
||||
if closed:
|
||||
from strategy_snapshot_lib import save_trend_plan_snapshot
|
||||
|
||||
save_trend_plan_snapshot(
|
||||
cfg,
|
||||
conn,
|
||||
closed,
|
||||
result_label=result_label,
|
||||
exit_price=float(exit_price),
|
||||
pnl_amount=float(pnl_amount) if pnl_amount is not None else None,
|
||||
)
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
if _trend_plan_trade_exists(conn, plan_id):
|
||||
return
|
||||
session_date = row["session_date"] or m.get_trading_day()
|
||||
@@ -937,6 +957,18 @@ def apply_manual_breakeven(cfg: dict, conn, row, offset_pct=None) -> tuple[bool,
|
||||
if callable(wl):
|
||||
lines.insert(1, f"**账户:{wl()}**")
|
||||
send("\n".join(lines))
|
||||
try:
|
||||
handoff = conn.execute(
|
||||
"SELECT * FROM trend_pullback_plans WHERE id=?", (plan_id,)
|
||||
).fetchone()
|
||||
if handoff:
|
||||
from strategy_snapshot_lib import save_trend_plan_snapshot
|
||||
|
||||
save_trend_plan_snapshot(
|
||||
cfg, conn, handoff, result_label="保本移交", exit_price=None, pnl_amount=None
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return True, None
|
||||
|
||||
|
||||
|
||||
@@ -77,6 +77,32 @@ DEFAULT_TREND_DISABLED_NOTE = (
|
||||
)
|
||||
|
||||
|
||||
def strategy_render_extras(
|
||||
conn,
|
||||
page: str,
|
||||
*,
|
||||
default_risk_percent: float = 2.0,
|
||||
count_active_trends: Optional[Callable] = None,
|
||||
trend_disabled_note: str = "",
|
||||
request_obj=None,
|
||||
trend_cfg: Optional[dict] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""render_main_page 策略相关页变量(含策略交易记录)。"""
|
||||
if page == "strategy_records":
|
||||
from strategy_records_register import load_strategy_records_page
|
||||
|
||||
return load_strategy_records_page(conn)
|
||||
return strategy_page_template_vars(
|
||||
conn,
|
||||
page,
|
||||
default_risk_percent=default_risk_percent,
|
||||
count_active_trends=count_active_trends,
|
||||
trend_disabled_note=trend_disabled_note,
|
||||
request_obj=request_obj,
|
||||
trend_cfg=trend_cfg,
|
||||
)
|
||||
|
||||
|
||||
def strategy_page_template_vars(
|
||||
conn,
|
||||
page: str,
|
||||
|
||||
Reference in New Issue
Block a user