fix: label trend breakeven handoff as 趋势回调 across four exchanges
Set order monitor and trade record source to trend pullback after handoff; unify hub and instance display; add migration script for legacy rows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -49,8 +49,10 @@ from fib_key_monitor_lib import (
|
|||||||
)
|
)
|
||||||
from strategy_trade_labels import (
|
from strategy_trade_labels import (
|
||||||
STRATEGY_ENTRY_REASON_OPTIONS,
|
STRATEGY_ENTRY_REASON_OPTIONS,
|
||||||
|
apply_order_monitor_source_labels,
|
||||||
entry_reason_for_monitor_type,
|
entry_reason_for_monitor_type,
|
||||||
handoff_trade_miss_reason,
|
handoff_trade_miss_reason,
|
||||||
|
order_monitor_source_type,
|
||||||
trade_record_monitor_type as resolve_trade_record_monitor_type,
|
trade_record_monitor_type as resolve_trade_record_monitor_type,
|
||||||
trend_plan_id_from_monitor_row,
|
trend_plan_id_from_monitor_row,
|
||||||
)
|
)
|
||||||
@@ -2556,9 +2558,7 @@ def enrich_order_item(raw_item, current_capital):
|
|||||||
item["breakeven_enabled"] = 0 if be is not None and int(be) == 0 else 1
|
item["breakeven_enabled"] = 0 if be is not None and int(be) == 0 else 1
|
||||||
except Exception:
|
except Exception:
|
||||||
item["breakeven_enabled"] = 1
|
item["breakeven_enabled"] = 1
|
||||||
if not (item.get("monitor_type") or "").strip():
|
return apply_order_monitor_source_labels(item, default_manual=ORDER_MONITOR_TYPE_MANUAL)
|
||||||
item["monitor_type"] = ORDER_MONITOR_TYPE_MANUAL
|
|
||||||
return item
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_exchange_live_ready():
|
def ensure_exchange_live_ready():
|
||||||
@@ -2570,17 +2570,7 @@ def ensure_exchange_live_ready():
|
|||||||
|
|
||||||
|
|
||||||
def order_row_monitor_type(row):
|
def order_row_monitor_type(row):
|
||||||
if row is None:
|
return order_monitor_source_type(row, default_manual=ORDER_MONITOR_TYPE_MANUAL)
|
||||||
return ORDER_MONITOR_TYPE_MANUAL
|
|
||||||
try:
|
|
||||||
keys = row.keys() if hasattr(row, "keys") else []
|
|
||||||
except Exception:
|
|
||||||
keys = []
|
|
||||||
if "monitor_type" in keys:
|
|
||||||
mt = (row["monitor_type"] or "").strip()
|
|
||||||
if mt:
|
|
||||||
return mt
|
|
||||||
return ORDER_MONITOR_TYPE_MANUAL
|
|
||||||
|
|
||||||
|
|
||||||
def trade_record_monitor_type(conn, row):
|
def trade_record_monitor_type(conn, row):
|
||||||
|
|||||||
@@ -50,8 +50,10 @@ from fib_key_monitor_lib import (
|
|||||||
)
|
)
|
||||||
from strategy_trade_labels import (
|
from strategy_trade_labels import (
|
||||||
STRATEGY_ENTRY_REASON_OPTIONS,
|
STRATEGY_ENTRY_REASON_OPTIONS,
|
||||||
|
apply_order_monitor_source_labels,
|
||||||
entry_reason_for_monitor_type,
|
entry_reason_for_monitor_type,
|
||||||
handoff_trade_miss_reason,
|
handoff_trade_miss_reason,
|
||||||
|
order_monitor_source_type,
|
||||||
trade_record_monitor_type as resolve_trade_record_monitor_type,
|
trade_record_monitor_type as resolve_trade_record_monitor_type,
|
||||||
trend_plan_id_from_monitor_row,
|
trend_plan_id_from_monitor_row,
|
||||||
)
|
)
|
||||||
@@ -2282,9 +2284,7 @@ def enrich_order_item(raw_item, current_capital):
|
|||||||
item["breakeven_enabled"] = 0 if be is not None and int(be) == 0 else 1
|
item["breakeven_enabled"] = 0 if be is not None and int(be) == 0 else 1
|
||||||
except Exception:
|
except Exception:
|
||||||
item["breakeven_enabled"] = 1
|
item["breakeven_enabled"] = 1
|
||||||
if not (item.get("monitor_type") or "").strip():
|
return apply_order_monitor_source_labels(item, default_manual=ORDER_MONITOR_TYPE_MANUAL)
|
||||||
item["monitor_type"] = ORDER_MONITOR_TYPE_MANUAL
|
|
||||||
return item
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_exchange_live_ready():
|
def ensure_exchange_live_ready():
|
||||||
@@ -2296,17 +2296,7 @@ def ensure_exchange_live_ready():
|
|||||||
|
|
||||||
|
|
||||||
def order_row_monitor_type(row):
|
def order_row_monitor_type(row):
|
||||||
if row is None:
|
return order_monitor_source_type(row, default_manual=ORDER_MONITOR_TYPE_MANUAL)
|
||||||
return ORDER_MONITOR_TYPE_MANUAL
|
|
||||||
try:
|
|
||||||
keys = row.keys() if hasattr(row, "keys") else []
|
|
||||||
except Exception:
|
|
||||||
keys = []
|
|
||||||
if "monitor_type" in keys:
|
|
||||||
mt = (row["monitor_type"] or "").strip()
|
|
||||||
if mt:
|
|
||||||
return mt
|
|
||||||
return ORDER_MONITOR_TYPE_MANUAL
|
|
||||||
|
|
||||||
|
|
||||||
def trade_record_monitor_type(conn, row):
|
def trade_record_monitor_type(conn, row):
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ from journal_chart_lib import (
|
|||||||
from hub_auth import request_allowed as hub_request_allowed
|
from hub_auth import request_allowed as hub_request_allowed
|
||||||
from strategy_trade_labels import (
|
from strategy_trade_labels import (
|
||||||
STRATEGY_ENTRY_REASON_OPTIONS,
|
STRATEGY_ENTRY_REASON_OPTIONS,
|
||||||
|
apply_order_monitor_source_labels,
|
||||||
handoff_trade_miss_reason,
|
handoff_trade_miss_reason,
|
||||||
trade_record_monitor_type as resolve_trade_record_monitor_type,
|
trade_record_monitor_type as resolve_trade_record_monitor_type,
|
||||||
trend_plan_id_from_monitor_row,
|
trend_plan_id_from_monitor_row,
|
||||||
@@ -2311,7 +2312,7 @@ def enrich_order_item(raw_item, current_capital):
|
|||||||
item["breakeven_enabled"] = 0 if be is not None and int(be) == 0 else 1
|
item["breakeven_enabled"] = 0 if be is not None and int(be) == 0 else 1
|
||||||
except Exception:
|
except Exception:
|
||||||
item["breakeven_enabled"] = 1
|
item["breakeven_enabled"] = 1
|
||||||
return item
|
return apply_order_monitor_source_labels(item, default_manual="下单监控")
|
||||||
|
|
||||||
|
|
||||||
def ensure_exchange_live_ready():
|
def ensure_exchange_live_ready():
|
||||||
|
|||||||
@@ -49,8 +49,10 @@ from fib_key_monitor_lib import (
|
|||||||
)
|
)
|
||||||
from strategy_trade_labels import (
|
from strategy_trade_labels import (
|
||||||
STRATEGY_ENTRY_REASON_OPTIONS,
|
STRATEGY_ENTRY_REASON_OPTIONS,
|
||||||
|
apply_order_monitor_source_labels,
|
||||||
entry_reason_for_monitor_type,
|
entry_reason_for_monitor_type,
|
||||||
handoff_trade_miss_reason,
|
handoff_trade_miss_reason,
|
||||||
|
order_monitor_source_type,
|
||||||
trade_record_monitor_type as resolve_trade_record_monitor_type,
|
trade_record_monitor_type as resolve_trade_record_monitor_type,
|
||||||
trend_plan_id_from_monitor_row,
|
trend_plan_id_from_monitor_row,
|
||||||
)
|
)
|
||||||
@@ -2138,7 +2140,7 @@ def enrich_order_item(raw_item, current_capital):
|
|||||||
item["breakeven_enabled"] = 0 if be is not None and int(be) == 0 else 1
|
item["breakeven_enabled"] = 0 if be is not None and int(be) == 0 else 1
|
||||||
except Exception:
|
except Exception:
|
||||||
item["breakeven_enabled"] = 1
|
item["breakeven_enabled"] = 1
|
||||||
return item
|
return apply_order_monitor_source_labels(item, default_manual=ORDER_MONITOR_TYPE_MANUAL)
|
||||||
|
|
||||||
|
|
||||||
def ensure_okx_live_ready():
|
def ensure_okx_live_ready():
|
||||||
@@ -2150,17 +2152,7 @@ def ensure_okx_live_ready():
|
|||||||
|
|
||||||
|
|
||||||
def order_row_monitor_type(row):
|
def order_row_monitor_type(row):
|
||||||
if row is None:
|
return order_monitor_source_type(row, default_manual=ORDER_MONITOR_TYPE_MANUAL)
|
||||||
return ORDER_MONITOR_TYPE_MANUAL
|
|
||||||
try:
|
|
||||||
keys = row.keys() if hasattr(row, "keys") else []
|
|
||||||
except Exception:
|
|
||||||
keys = []
|
|
||||||
if "monitor_type" in keys:
|
|
||||||
mt = (row["monitor_type"] or "").strip()
|
|
||||||
if mt:
|
|
||||||
return mt
|
|
||||||
return ORDER_MONITOR_TYPE_MANUAL
|
|
||||||
|
|
||||||
|
|
||||||
def trade_record_monitor_type(conn, row):
|
def trade_record_monitor_type(conn, row):
|
||||||
|
|||||||
+8
-1
@@ -283,7 +283,14 @@ def register_hub_routes(app):
|
|||||||
for row in conn.execute(
|
for row in conn.execute(
|
||||||
"SELECT * FROM order_monitors WHERE status='active' ORDER BY id DESC"
|
"SELECT * FROM order_monitors WHERE status='active' ORDER BY id DESC"
|
||||||
).fetchall():
|
).fetchall():
|
||||||
orders.append(_row_to_dict(row))
|
od = _row_to_dict(row)
|
||||||
|
try:
|
||||||
|
from strategy_trade_labels import apply_order_monitor_source_labels
|
||||||
|
|
||||||
|
od = apply_order_monitor_source_labels(od)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
orders.append(od)
|
||||||
trends = []
|
trends = []
|
||||||
if c.get("has_trend"):
|
if c.get("has_trend"):
|
||||||
for row in conn.execute(
|
for row in conn.execute(
|
||||||
|
|||||||
@@ -303,6 +303,27 @@
|
|||||||
return side || "—";
|
return side || "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function monitorOrderSourceLabel(mo) {
|
||||||
|
const o = mo || {};
|
||||||
|
const tid = Number(o.trend_plan_id);
|
||||||
|
if (Number.isFinite(tid) && tid > 0) return "趋势回调";
|
||||||
|
const mt = String(o.monitor_type || "").trim();
|
||||||
|
if (mt === "趋势回调") return "趋势回调";
|
||||||
|
const kst = String(o.key_signal_type || "").trim();
|
||||||
|
if (kst === "趋势回调" || kst === "趋势回调计划") return "趋势回调";
|
||||||
|
return mt || "下单监控";
|
||||||
|
}
|
||||||
|
|
||||||
|
function monitorOrderSourceHtml(mo) {
|
||||||
|
const src = monitorOrderSourceLabel(mo);
|
||||||
|
const kst = String((mo && mo.key_signal_type) || "").trim();
|
||||||
|
let text = src;
|
||||||
|
if (kst && kst !== src && !text.includes(kst)) {
|
||||||
|
text += " · " + kst;
|
||||||
|
}
|
||||||
|
return `来源: ${esc(text)}`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderDirectionHtml(side) {
|
function renderDirectionHtml(side) {
|
||||||
const cls = sideDirCls(side);
|
const cls = sideDirCls(side);
|
||||||
const label = sideDirLabel(side);
|
const label = sideDirLabel(side);
|
||||||
@@ -1052,7 +1073,9 @@
|
|||||||
: tp.avg_entry_price;
|
: tp.avg_entry_price;
|
||||||
const entryN = entryRaw != null && entryRaw !== "" ? Number(entryRaw) : null;
|
const entryN = entryRaw != null && entryRaw !== "" ? Number(entryRaw) : null;
|
||||||
const isTrend =
|
const isTrend =
|
||||||
!!(trendPlan && trendPlan.id) || String(mo.monitor_type || "").trim() === "趋势回调";
|
!!(trendPlan && trendPlan.id) ||
|
||||||
|
String(mo.monitor_type || "").trim() === "趋势回调" ||
|
||||||
|
(mo.trend_plan_id != null && Number(mo.trend_plan_id) > 0);
|
||||||
|
|
||||||
let sl = mo.stop_loss != null && mo.stop_loss !== "" ? mo.stop_loss : "";
|
let sl = mo.stop_loss != null && mo.stop_loss !== "" ? mo.stop_loss : "";
|
||||||
let takeProfit = mo.take_profit != null && mo.take_profit !== "" ? mo.take_profit : "";
|
let takeProfit = mo.take_profit != null && mo.take_profit !== "" ? mo.take_profit : "";
|
||||||
@@ -1422,10 +1445,8 @@
|
|||||||
pnlText += ` (${pct >= 0 ? "" : ""}${pct.toFixed(2)}%)`;
|
pnlText += ` (${pct >= 0 ? "" : ""}${pct.toFixed(2)}%)`;
|
||||||
}
|
}
|
||||||
const meta = [];
|
const meta = [];
|
||||||
if (mo.monitor_type || mo.key_signal_type) {
|
if (mo.monitor_type || mo.key_signal_type || mo.trend_plan_id) {
|
||||||
meta.push(
|
meta.push(monitorOrderSourceHtml(mo));
|
||||||
`来源: ${esc(mo.monitor_type || "下单监控")}${mo.key_signal_type ? " · " + esc(mo.key_signal_type) : ""}`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
meta.push("来源: 交易所持仓");
|
meta.push("来源: 交易所持仓");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,6 +229,6 @@
|
|||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||||
<script src="/assets/chart.js?v=20260603-hub-binance-tick"></script>
|
<script src="/assets/chart.js?v=20260603-hub-binance-tick"></script>
|
||||||
<script src="/assets/app.js?v=20260604-hub-stat-colors"></script>
|
<script src="/assets/app.js?v=20260604-trend-handoff-src"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""修正趋势保本移交后 monitor_type 仍为「下单监控」的历史数据。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from strategy_trade_labels import MONITOR_TYPE_TREND_PULLBACK
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Fix trend handoff order/trade monitor_type labels.")
|
||||||
|
parser.add_argument("--db", required=True, help="Path to instance sqlite db")
|
||||||
|
parser.add_argument("--dry-run", action="store_true", help="Preview only")
|
||||||
|
parser.add_argument("--apply", action="store_true", help="Apply updates")
|
||||||
|
args = parser.parse_args()
|
||||||
|
if not args.dry_run and not args.apply:
|
||||||
|
args.dry_run = True
|
||||||
|
|
||||||
|
db_path = Path(args.db).expanduser().resolve()
|
||||||
|
if not db_path.is_file():
|
||||||
|
print(f"[ERR] DB not found: {db_path}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
conn = sqlite3.connect(str(db_path))
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*) AS c FROM order_monitors
|
||||||
|
WHERE trend_plan_id IS NOT NULL AND trend_plan_id > 0
|
||||||
|
AND (monitor_type IS NULL OR TRIM(monitor_type) = '' OR monitor_type = '下单监控')
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
om_n = int(cur.fetchone()["c"])
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*) AS c FROM trade_records
|
||||||
|
WHERE trend_plan_id IS NOT NULL AND trend_plan_id > 0
|
||||||
|
AND (monitor_type IS NULL OR TRIM(monitor_type) = '' OR monitor_type = '下单监控')
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
tr_n = int(cur.fetchone()["c"])
|
||||||
|
print(f"[INFO] order_monitors to fix: {om_n}")
|
||||||
|
print(f"[INFO] trade_records to fix: {tr_n}")
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
conn.close()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
UPDATE order_monitors
|
||||||
|
SET monitor_type=?
|
||||||
|
WHERE trend_plan_id IS NOT NULL AND trend_plan_id > 0
|
||||||
|
AND (monitor_type IS NULL OR TRIM(monitor_type) = '' OR monitor_type = '下单监控')
|
||||||
|
""",
|
||||||
|
(MONITOR_TYPE_TREND_PULLBACK,),
|
||||||
|
)
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
UPDATE trade_records
|
||||||
|
SET monitor_type=?
|
||||||
|
WHERE trend_plan_id IS NOT NULL AND trend_plan_id > 0
|
||||||
|
AND (monitor_type IS NULL OR TRIM(monitor_type) = '' OR monitor_type = '下单监控')
|
||||||
|
""",
|
||||||
|
(MONITOR_TYPE_TREND_PULLBACK,),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print("[OK] Applied.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -81,8 +81,45 @@ def _row_monitor_type(row, default_manual: str) -> str:
|
|||||||
return default_manual
|
return default_manual
|
||||||
|
|
||||||
|
|
||||||
|
def _row_key_signal_type(row) -> str:
|
||||||
|
if row is None:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
keys = row.keys() if hasattr(row, "keys") else []
|
||||||
|
except Exception:
|
||||||
|
keys = []
|
||||||
|
if "key_signal_type" not in keys:
|
||||||
|
return ""
|
||||||
|
return (row["key_signal_type"] or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def order_monitor_source_type(row, *, default_manual: str = "下单监控") -> str:
|
||||||
|
"""展示/平仓记录:趋势保本移交单来源为「趋势回调」,非「下单监控」。"""
|
||||||
|
if trend_plan_id_from_monitor_row(row) is not None:
|
||||||
|
return MONITOR_TYPE_TREND_PULLBACK
|
||||||
|
mt = _row_monitor_type(row, default_manual)
|
||||||
|
if mt != default_manual:
|
||||||
|
return mt
|
||||||
|
kst = _row_key_signal_type(row)
|
||||||
|
if kst in (
|
||||||
|
MONITOR_TYPE_TREND_PULLBACK,
|
||||||
|
TREND_HANDOFF_KEY_SIGNAL,
|
||||||
|
TREND_HANDOFF_TRADE_NOTE,
|
||||||
|
ENTRY_REASON_TREND_PULLBACK,
|
||||||
|
):
|
||||||
|
return MONITOR_TYPE_TREND_PULLBACK
|
||||||
|
return mt
|
||||||
|
|
||||||
|
|
||||||
|
def apply_order_monitor_source_labels(item: dict, *, default_manual: str = "下单监控") -> dict:
|
||||||
|
"""实例页 / 中控 API:统一修正 order_monitors 展示用 monitor_type。"""
|
||||||
|
out = dict(item or {})
|
||||||
|
out["monitor_type"] = order_monitor_source_type(out, default_manual=default_manual)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def trade_record_monitor_type(conn, order_row, *, default_manual: str = "下单监控") -> str:
|
def trade_record_monitor_type(conn, order_row, *, default_manual: str = "下单监控") -> str:
|
||||||
"""平仓写入 trade_records 时:曾顺势加仓则标「顺势加仓」,否则沿用监控单类型。"""
|
"""平仓写入 trade_records 时:曾顺势加仓则标「顺势加仓」,否则沿用监控单来源类型。"""
|
||||||
oid = None
|
oid = None
|
||||||
try:
|
try:
|
||||||
keys = order_row.keys() if hasattr(order_row, "keys") else []
|
keys = order_row.keys() if hasattr(order_row, "keys") else []
|
||||||
@@ -92,7 +129,7 @@ def trade_record_monitor_type(conn, order_row, *, default_manual: str = "下单
|
|||||||
oid = None
|
oid = None
|
||||||
if oid and order_had_roll_fills(conn, oid):
|
if oid and order_had_roll_fills(conn, oid):
|
||||||
return MONITOR_TYPE_ROLL
|
return MONITOR_TYPE_ROLL
|
||||||
return _row_monitor_type(order_row, default_manual)
|
return order_monitor_source_type(order_row, default_manual=default_manual)
|
||||||
|
|
||||||
|
|
||||||
def entry_reason_for_monitor_type(monitor_type: str | None) -> str:
|
def entry_reason_for_monitor_type(monitor_type: str | None) -> str:
|
||||||
|
|||||||
@@ -748,7 +748,7 @@ def _insert_trend_handoff_order_monitor(
|
|||||||
if not trading_day and callable(getattr(m, "get_trading_day", None)):
|
if not trading_day and callable(getattr(m, "get_trading_day", None)):
|
||||||
trading_day = m.get_trading_day()
|
trading_day = m.get_trading_day()
|
||||||
notional = margin_cap * lev if margin_cap and lev else None
|
notional = margin_cap * lev if margin_cap and lev else None
|
||||||
monitor_type = _order_monitor_manual_type(m)
|
monitor_type = MONITOR_TYPE_TREND_PULLBACK
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO order_monitors "
|
"INSERT INTO order_monitors "
|
||||||
"(symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, "
|
"(symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, "
|
||||||
|
|||||||
Reference in New Issue
Block a user