1b3f661bad
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>
142 lines
4.4 KiB
Python
142 lines
4.4 KiB
Python
"""策略交易写入 trade_records 时的类型与复盘开仓类型标注。"""
|
||
from __future__ import annotations
|
||
|
||
from typing import Optional
|
||
|
||
MONITOR_TYPE_TREND_PULLBACK = "趋势回调"
|
||
MONITOR_TYPE_ROLL = "顺势加仓"
|
||
|
||
ENTRY_REASON_TREND_PULLBACK = "趋势回调"
|
||
ENTRY_REASON_ROLL = "顺势加仓"
|
||
|
||
STRATEGY_ENTRY_REASON_OPTIONS = (
|
||
ENTRY_REASON_TREND_PULLBACK,
|
||
ENTRY_REASON_ROLL,
|
||
)
|
||
|
||
# 趋势回调保本移交下单监控:order_monitors.key_signal_type / 平仓备注
|
||
TREND_HANDOFF_KEY_SIGNAL = ENTRY_REASON_TREND_PULLBACK
|
||
TREND_HANDOFF_TRADE_NOTE = "趋势回调计划"
|
||
|
||
|
||
def handoff_trade_miss_reason(miss_reason, row) -> Optional[str]:
|
||
"""趋势保本移交的监控单平仓:交易记录备注带来源。"""
|
||
if trend_plan_id_from_monitor_row(row) is None:
|
||
return miss_reason
|
||
base = (miss_reason or "").strip()
|
||
if TREND_HANDOFF_TRADE_NOTE in base:
|
||
return base or TREND_HANDOFF_TRADE_NOTE
|
||
if base:
|
||
return f"{TREND_HANDOFF_TRADE_NOTE};{base}"
|
||
return TREND_HANDOFF_TRADE_NOTE
|
||
|
||
|
||
def trend_plan_id_from_monitor_row(row) -> Optional[int]:
|
||
if row is None:
|
||
return None
|
||
try:
|
||
keys = row.keys() if hasattr(row, "keys") else []
|
||
except Exception:
|
||
keys = []
|
||
if "trend_plan_id" not in keys or row["trend_plan_id"] in (None, ""):
|
||
return None
|
||
try:
|
||
tid = int(row["trend_plan_id"])
|
||
return tid if tid > 0 else None
|
||
except (TypeError, ValueError):
|
||
return None
|
||
|
||
|
||
def order_had_roll_fills(conn, order_monitor_id) -> bool:
|
||
try:
|
||
oid = int(order_monitor_id)
|
||
except (TypeError, ValueError):
|
||
return False
|
||
if oid <= 0:
|
||
return False
|
||
try:
|
||
row = conn.execute(
|
||
"""SELECT 1 FROM roll_legs l
|
||
INNER JOIN roll_groups g ON g.id = l.roll_group_id
|
||
WHERE g.order_monitor_id=? AND l.status='filled'
|
||
LIMIT 1""",
|
||
(oid,),
|
||
).fetchone()
|
||
return row is not None
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def _row_monitor_type(row, default_manual: str) -> str:
|
||
if row is None:
|
||
return default_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 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:
|
||
"""平仓写入 trade_records 时:曾顺势加仓则标「顺势加仓」,否则沿用监控单来源类型。"""
|
||
oid = None
|
||
try:
|
||
keys = order_row.keys() if hasattr(order_row, "keys") else []
|
||
if "id" in keys and order_row["id"] is not None:
|
||
oid = int(order_row["id"])
|
||
except Exception:
|
||
oid = None
|
||
if oid and order_had_roll_fills(conn, oid):
|
||
return MONITOR_TYPE_ROLL
|
||
return order_monitor_source_type(order_row, default_manual=default_manual)
|
||
|
||
|
||
def entry_reason_for_monitor_type(monitor_type: str | None) -> str:
|
||
mt = (monitor_type or "").strip()
|
||
if mt == MONITOR_TYPE_TREND_PULLBACK:
|
||
return ENTRY_REASON_TREND_PULLBACK
|
||
if mt == MONITOR_TYPE_ROLL:
|
||
return ENTRY_REASON_ROLL
|
||
return ""
|