顺势加仓 v2:程序监控滚仓、文档页与平仓同步
重写滚仓计仓与四种加仓方式(市价/斐波/突破),程序盯 mark 触价成交;风险读监控单;pending 可删不可改;手动平仓同步结束滚仓。新增 /strategy/roll/docs 说明页与顺势加仓滚仓说明.md。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+317
-125
@@ -1,17 +1,29 @@
|
||||
"""滚仓挂单监控:斐波限价止盈侧突破撤单、成交同步、活跃组结案(各所共用)。"""
|
||||
"""滚仓程序监控:斐波/突破触价市价成交、失效、外部平仓同步(各所共用)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from fib_key_monitor_lib import fib_invalidate_by_mark
|
||||
from strategy_roll_lib import unified_stop_from_avg
|
||||
from strategy_roll_lib import (
|
||||
BREAKOUT_MODE,
|
||||
FIB_MODES,
|
||||
MARKET_MODE,
|
||||
mode_label,
|
||||
roll_breakout_invalidate,
|
||||
roll_breakout_trigger_crossed,
|
||||
roll_fib_invalidate,
|
||||
roll_fib_trigger_crossed,
|
||||
calc_risk_budget_usdt,
|
||||
max_roll_legs,
|
||||
preview_roll,
|
||||
solve_add_amount_for_total_risk,
|
||||
)
|
||||
from strategy_db import init_strategy_tables
|
||||
|
||||
ROLL_LEG_STATUS_LABELS = {
|
||||
"pending": "挂单中",
|
||||
"pending": "监控中",
|
||||
"filled": "已成交",
|
||||
"cancelled": "已撤销",
|
||||
"invalidated": "止盈侧突破失效",
|
||||
"cancelled": "已删除",
|
||||
"invalidated": "已失效",
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +52,97 @@ def check_roll_monitors(cfg: dict[str, Any]) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def sync_roll_after_external_close(
|
||||
cfg: dict, conn, symbol: str, direction: str, *, reason: str = "持仓已平"
|
||||
) -> dict[str, Any]:
|
||||
"""中控/实例手动平仓后:取消 pending 腿并关闭 active 滚仓组(保留 filled 历史)。"""
|
||||
norm = cfg.get("normalize_symbol_input")
|
||||
sym = norm(symbol) if callable(norm) else (symbol or "").strip()
|
||||
if not sym:
|
||||
return {"ok": False, "msg": "symbol 无效", "closed_groups": 0, "cancelled_legs": 0}
|
||||
direction = (direction or "long").strip().lower()
|
||||
init_strategy_tables(conn)
|
||||
rows = conn.execute(
|
||||
"""SELECT g.* FROM roll_groups g
|
||||
WHERE g.status='active' AND g.symbol=? AND g.direction=?""",
|
||||
(sym, direction),
|
||||
).fetchall()
|
||||
closed = cancelled = 0
|
||||
for row in rows:
|
||||
g = _row_dict(row)
|
||||
cancelled += _cancel_pending_legs_for_group(conn, cfg, g, status="cancelled")
|
||||
cur = conn.execute(
|
||||
"UPDATE roll_groups SET status='closed', updated_at=? WHERE id=? AND status='active'",
|
||||
(_now(cfg), int(g["id"])),
|
||||
)
|
||||
if getattr(cur, "rowcount", 0):
|
||||
closed += 1
|
||||
try:
|
||||
from strategy_wechat_notify import notify_roll_group_ended
|
||||
|
||||
notify_roll_group_ended(
|
||||
cfg,
|
||||
group_id=int(g["id"]),
|
||||
symbol=sym,
|
||||
direction=direction,
|
||||
reason=reason,
|
||||
leg_count=int(g.get("leg_count") or 0),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from strategy_snapshot_lib import save_roll_group_snapshot
|
||||
|
||||
save_roll_group_snapshot(cfg, conn, g, result_label="结束")
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"ok": True,
|
||||
"symbol": sym,
|
||||
"direction": direction,
|
||||
"closed_groups": closed,
|
||||
"cancelled_legs": cancelled,
|
||||
}
|
||||
|
||||
|
||||
def cancel_roll_pending_leg(cfg: dict, conn, leg_id: int) -> tuple[bool, str]:
|
||||
"""用户删除 pending 滚仓腿(不可修改,仅删除)。"""
|
||||
init_strategy_tables(conn)
|
||||
row = conn.execute(
|
||||
"SELECT l.*, g.symbol, g.direction, g.status AS group_status FROM roll_legs l "
|
||||
"INNER JOIN roll_groups g ON g.id = l.roll_group_id WHERE l.id=?",
|
||||
(int(leg_id),),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return False, "滚仓腿不存在"
|
||||
leg = _row_dict(row)
|
||||
if (leg.get("status") or "").strip().lower() != "pending":
|
||||
return False, "仅监控中的腿可删除"
|
||||
_cancel_roll_leg_order(cfg, {"symbol": leg.get("symbol"), "exchange_symbol": leg.get("exchange_symbol")}, leg)
|
||||
conn.execute(
|
||||
"UPDATE roll_legs SET status='cancelled' WHERE id=? AND status='pending'",
|
||||
(int(leg_id),),
|
||||
)
|
||||
conn.commit()
|
||||
return True, "已删除滚仓监控"
|
||||
|
||||
|
||||
def count_filled_roll_legs(conn, roll_group_id: int) -> int:
|
||||
row = conn.execute(
|
||||
"SELECT COUNT(*) FROM roll_legs WHERE roll_group_id=? AND status='filled'",
|
||||
(int(roll_group_id),),
|
||||
).fetchone()
|
||||
return int(row[0] if row else 0)
|
||||
|
||||
|
||||
def count_pending_roll_legs(conn, roll_group_id: int) -> int:
|
||||
row = conn.execute(
|
||||
"SELECT COUNT(*) FROM roll_legs WHERE roll_group_id=? AND status='pending'",
|
||||
(int(roll_group_id),),
|
||||
).fetchone()
|
||||
return int(row[0] if row else 0)
|
||||
|
||||
|
||||
def _row_dict(row) -> dict:
|
||||
if row is None:
|
||||
return {}
|
||||
@@ -54,25 +157,26 @@ def _now(cfg: dict) -> str:
|
||||
return fn() if callable(fn) else ""
|
||||
|
||||
|
||||
def _close_roll_group(
|
||||
conn,
|
||||
cfg: dict,
|
||||
group: dict,
|
||||
*,
|
||||
cancel_pending: bool = True,
|
||||
) -> None:
|
||||
def _cancel_pending_legs_for_group(conn, cfg: dict, group: dict, *, status: str = "cancelled") -> int:
|
||||
gid = int(group["id"])
|
||||
if cancel_pending:
|
||||
for leg in conn.execute(
|
||||
"SELECT * FROM roll_legs WHERE roll_group_id=? AND status='pending'",
|
||||
(gid,),
|
||||
).fetchall():
|
||||
ld = _row_dict(leg)
|
||||
_cancel_roll_leg_order(cfg, group, ld)
|
||||
conn.execute(
|
||||
"UPDATE roll_legs SET status='cancelled' WHERE id=? AND status='pending'",
|
||||
(ld["id"],),
|
||||
)
|
||||
n = 0
|
||||
for leg in conn.execute(
|
||||
"SELECT * FROM roll_legs WHERE roll_group_id=? AND status='pending'",
|
||||
(gid,),
|
||||
).fetchall():
|
||||
ld = _row_dict(leg)
|
||||
_cancel_roll_leg_order(cfg, group, ld)
|
||||
conn.execute(
|
||||
"UPDATE roll_legs SET status=? WHERE id=? AND status='pending'",
|
||||
(status, ld["id"]),
|
||||
)
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def _close_roll_group(conn, cfg: dict, group: dict, *, reason: str = "下单监控已结案或交易所无同向持仓") -> None:
|
||||
gid = int(group["id"])
|
||||
_cancel_pending_legs_for_group(conn, cfg, group, status="cancelled")
|
||||
cur = conn.execute(
|
||||
"UPDATE roll_groups SET status='closed', updated_at=? WHERE id=? AND status='active'",
|
||||
(_now(cfg), gid),
|
||||
@@ -81,7 +185,6 @@ def _close_roll_group(
|
||||
try:
|
||||
from strategy_wechat_notify import notify_roll_group_ended
|
||||
|
||||
reason = "下单监控已结案或交易所无同向持仓"
|
||||
notify_roll_group_ended(
|
||||
cfg,
|
||||
group_id=gid,
|
||||
@@ -116,7 +219,7 @@ def _reconcile_roll_groups(conn, cfg: dict) -> None:
|
||||
pos = cfg["get_position"](ex_sym, direction)
|
||||
qty = float(pos.get("contracts") or 0)
|
||||
if not mon_ok or qty <= 0:
|
||||
_close_roll_group(conn, cfg, g, cancel_pending=True)
|
||||
_close_roll_group(conn, cfg, g)
|
||||
|
||||
|
||||
def _cancel_roll_leg_order(cfg: dict, group: dict, leg: dict) -> None:
|
||||
@@ -133,10 +236,35 @@ def _cancel_roll_leg_order(cfg: dict, group: dict, leg: dict) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _contract_size(cfg: dict, ex_sym: str) -> float:
|
||||
get_cs = cfg.get("get_contract_size")
|
||||
if callable(get_cs):
|
||||
try:
|
||||
return float(get_cs(ex_sym) or 1.0)
|
||||
except Exception:
|
||||
pass
|
||||
return 1.0
|
||||
|
||||
|
||||
def _resolve_add_mode(leg: dict) -> str:
|
||||
raw = (leg.get("add_mode") or "").strip().lower()
|
||||
if raw in (MARKET_MODE, "market", "市价", "市价加仓"):
|
||||
return MARKET_MODE
|
||||
if "786" in raw or raw == "fib_786":
|
||||
return "fib_786"
|
||||
if "618" in raw or raw == "fib_618":
|
||||
return "fib_618"
|
||||
if raw in (BREAKOUT_MODE, "突破", "突破加仓"):
|
||||
return BREAKOUT_MODE
|
||||
if raw.startswith("fib"):
|
||||
return raw.replace(".", "_").replace("0.", "0")
|
||||
return raw or MARKET_MODE
|
||||
|
||||
|
||||
def _check_pending_roll_legs(conn, cfg: dict) -> None:
|
||||
rows = conn.execute(
|
||||
"""SELECT l.*, g.symbol, g.exchange_symbol, g.direction, g.initial_take_profit,
|
||||
g.order_monitor_id
|
||||
g.order_monitor_id, g.risk_percent, g.leg_count
|
||||
FROM roll_legs l
|
||||
INNER JOIN roll_groups g ON g.id = l.roll_group_id AND g.status='active'
|
||||
WHERE l.status='pending'"""
|
||||
@@ -150,6 +278,8 @@ def _check_pending_roll_legs(conn, cfg: dict) -> None:
|
||||
"direction": leg["direction"],
|
||||
"initial_take_profit": leg["initial_take_profit"],
|
||||
"order_monitor_id": leg["order_monitor_id"],
|
||||
"risk_percent": leg.get("risk_percent"),
|
||||
"leg_count": leg.get("leg_count"),
|
||||
}
|
||||
_process_pending_roll_leg(conn, cfg, group, leg)
|
||||
|
||||
@@ -158,56 +288,51 @@ def _process_pending_roll_leg(conn, cfg: dict, group: dict, leg: dict) -> None:
|
||||
symbol = group.get("symbol") or ""
|
||||
direction = (group.get("direction") or "long").strip().lower()
|
||||
ex_sym = group.get("exchange_symbol") or cfg["normalize_exchange_symbol"](symbol)
|
||||
oid = (leg.get("exchange_order_id") or "").strip()
|
||||
mark_fn = cfg.get("get_mark_price") or cfg.get("get_price")
|
||||
mark = mark_fn(symbol) if callable(mark_fn) else None
|
||||
if mark is None:
|
||||
return
|
||||
mark_f = float(mark)
|
||||
prev_mark = leg.get("last_mark_price")
|
||||
try:
|
||||
prev_f = float(prev_mark) if prev_mark not in (None, "") else None
|
||||
except (TypeError, ValueError):
|
||||
prev_f = None
|
||||
|
||||
order_status_fn = cfg.get("limit_order_status")
|
||||
order_st = order_status_fn(ex_sym, oid) if callable(order_status_fn) and oid else "missing"
|
||||
|
||||
mode = _resolve_add_mode(leg)
|
||||
sl = float(leg.get("new_stop_loss") or 0)
|
||||
fib_u, fib_l = leg.get("fib_upper"), leg.get("fib_lower")
|
||||
has_fib = fib_u is not None and fib_l is not None
|
||||
bp = leg.get("breakthrough_price")
|
||||
|
||||
if order_st == "filled":
|
||||
_finalize_roll_leg_fill(conn, cfg, group, leg, ex_sym, direction, float(mark))
|
||||
return
|
||||
if mode in FIB_MODES and fib_u is not None and fib_l is not None:
|
||||
if roll_fib_invalidate(direction, mark_f, float(fib_u), float(fib_l)):
|
||||
_invalidate_roll_leg(conn, cfg, group, leg, mark_f, reason="止盈侧突破")
|
||||
return
|
||||
elif mode == BREAKOUT_MODE and sl > 0:
|
||||
if roll_breakout_invalidate(direction, mark_f, sl):
|
||||
_invalidate_roll_leg(conn, cfg, group, leg, mark_f, reason="止损侧突破")
|
||||
return
|
||||
|
||||
if has_fib and fib_invalidate_by_mark(direction, mark, fib_u, fib_l):
|
||||
if order_st == "open":
|
||||
_cancel_roll_leg_order(cfg, group, leg)
|
||||
_invalidate_roll_leg(conn, cfg, group, leg, float(mark))
|
||||
return
|
||||
triggered = False
|
||||
if mode in FIB_MODES:
|
||||
lp = leg.get("limit_price")
|
||||
if lp is not None and roll_fib_trigger_crossed(direction, prev_f, mark_f, float(lp)):
|
||||
triggered = True
|
||||
elif mode == BREAKOUT_MODE and bp is not None:
|
||||
if roll_breakout_trigger_crossed(direction, prev_f, mark_f, float(bp)):
|
||||
triggered = True
|
||||
|
||||
if order_st in ("canceled", "missing", "unknown") and has_fib:
|
||||
if fib_invalidate_by_mark(direction, mark, fib_u, fib_l):
|
||||
_invalidate_roll_leg(conn, cfg, group, leg, float(mark))
|
||||
|
||||
|
||||
def _invalidate_roll_leg(
|
||||
conn, cfg: dict, group: dict, leg: dict, mark: float
|
||||
) -> None:
|
||||
leg_id = int(leg["id"])
|
||||
gid = int(group["id"])
|
||||
cur = conn.execute(
|
||||
"SELECT status FROM roll_legs WHERE id=?", (leg_id,)
|
||||
).fetchone()
|
||||
if not cur or (cur[0] or "").strip().lower() == "invalidated":
|
||||
return
|
||||
conn.execute(
|
||||
"UPDATE roll_legs SET status='invalidated' WHERE id=? AND status='pending'",
|
||||
(leg_id,),
|
||||
"UPDATE roll_legs SET last_mark_price=? WHERE id=? AND status='pending'",
|
||||
(mark_f, int(leg["id"])),
|
||||
)
|
||||
conn.execute(
|
||||
"""UPDATE roll_groups SET leg_count = CASE WHEN leg_count > 0 THEN leg_count - 1 ELSE 0 END,
|
||||
updated_at=? WHERE id=?""",
|
||||
(_now(cfg), gid),
|
||||
)
|
||||
_send_roll_invalidate_wechat(cfg, group, leg, mark)
|
||||
|
||||
if triggered:
|
||||
_execute_pending_roll_leg(conn, cfg, group, leg, ex_sym, direction, mark_f)
|
||||
return
|
||||
|
||||
|
||||
def _finalize_roll_leg_fill(
|
||||
def _execute_pending_roll_leg(
|
||||
conn,
|
||||
cfg: dict,
|
||||
group: dict,
|
||||
@@ -217,94 +342,161 @@ def _finalize_roll_leg_fill(
|
||||
mark: float,
|
||||
) -> None:
|
||||
leg_id = int(leg["id"])
|
||||
gid = int(group["id"])
|
||||
new_sl = float(leg.get("new_stop_loss") or 0)
|
||||
stop_offset_pct = leg.get("stop_offset_pct")
|
||||
tp0 = float(group.get("initial_take_profit") or 0)
|
||||
fill_px = float(leg.get("limit_price") or mark)
|
||||
add_qty = float(leg.get("amount") or 0)
|
||||
if stop_offset_pct not in (None, ""):
|
||||
try:
|
||||
offset_pct = float(stop_offset_pct)
|
||||
except (TypeError, ValueError):
|
||||
offset_pct = 0.0
|
||||
if offset_pct > 0:
|
||||
pos = cfg["get_position"](ex_sym, direction) or {}
|
||||
avg = float(pos.get("entry_price") or 0)
|
||||
if avg <= 0 and add_qty > 0:
|
||||
avg = fill_px
|
||||
if avg > 0:
|
||||
new_sl = unified_stop_from_avg(direction, avg, offset_pct)
|
||||
px_fn = cfg.get("price_to_precision")
|
||||
if callable(px_fn):
|
||||
try:
|
||||
new_sl = float(px_fn(ex_sym, new_sl) or new_sl)
|
||||
except Exception:
|
||||
pass
|
||||
conn.execute(
|
||||
"UPDATE roll_legs SET status='filled', fill_price=?, new_stop_loss=? WHERE id=? AND status='pending'",
|
||||
(fill_px, new_sl, leg_id),
|
||||
)
|
||||
if new_sl > 0:
|
||||
conn.execute(
|
||||
"UPDATE roll_groups SET current_stop_loss=?, updated_at=? WHERE id=?",
|
||||
(new_sl, _now(cfg), gid),
|
||||
)
|
||||
gid = int(group["roll_group_id"]) if "roll_group_id" in leg else int(group["id"])
|
||||
mon_id = group.get("order_monitor_id")
|
||||
if mon_id and new_sl > 0:
|
||||
conn.execute(
|
||||
"UPDATE order_monitors SET stop_loss=? WHERE id=? AND status='active'",
|
||||
(new_sl, mon_id),
|
||||
mon = None
|
||||
if mon_id:
|
||||
row = conn.execute("SELECT * FROM order_monitors WHERE id=?", (mon_id,)).fetchone()
|
||||
mon = _row_dict(row) if row else None
|
||||
if not mon or (mon.get("status") or "").strip().lower() != "active":
|
||||
_invalidate_roll_leg(conn, cfg, group, leg, mark, reason="监控单已失效")
|
||||
return
|
||||
|
||||
pos = cfg["get_position"](ex_sym, direction) or {}
|
||||
qty = float(pos.get("contracts") or 0)
|
||||
entry = float(pos.get("entry_price") or mon.get("trigger_price") or 0)
|
||||
if qty <= 0 or entry <= 0:
|
||||
_invalidate_roll_leg(conn, cfg, group, leg, mark, reason="无持仓")
|
||||
return
|
||||
|
||||
filled = count_filled_roll_legs(conn, gid)
|
||||
if filled >= max_roll_legs(direction):
|
||||
_invalidate_roll_leg(conn, cfg, group, leg, mark, reason="滚仓次数已满")
|
||||
return
|
||||
|
||||
try:
|
||||
risk_pct = float(mon.get("risk_percent") or group.get("risk_percent") or 2)
|
||||
except (TypeError, ValueError):
|
||||
risk_pct = 2.0
|
||||
conn_cap = cfg["get_db"]()
|
||||
try:
|
||||
capital = float(cfg["get_trading_capital_usdt"](conn_cap))
|
||||
finally:
|
||||
conn_cap.close()
|
||||
|
||||
cs = _contract_size(cfg, ex_sym)
|
||||
sl = float(leg.get("new_stop_loss") or 0)
|
||||
tp0 = float(group.get("initial_take_profit") or mon.get("take_profit") or 0)
|
||||
mode = _resolve_add_mode(leg)
|
||||
|
||||
q2_raw, err = solve_add_amount_for_total_risk(
|
||||
direction, qty, entry, mark, sl, calc_risk_budget_usdt(capital, risk_pct), cs
|
||||
)
|
||||
if err or q2_raw is None or float(q2_raw) <= 0:
|
||||
_invalidate_roll_leg(conn, cfg, group, leg, mark, reason=err or "无法计算加仓张数")
|
||||
return
|
||||
|
||||
amount = cfg["amount_to_precision"](ex_sym, float(q2_raw))
|
||||
if amount is None or float(amount) <= 0:
|
||||
_invalidate_roll_leg(conn, cfg, group, leg, mark, reason="加仓张数低于交易所最小精度")
|
||||
return
|
||||
|
||||
lev_fn = cfg.get("default_leverage")
|
||||
if not callable(lev_fn):
|
||||
lev_fn = lambda _s: 5
|
||||
leverage = int(lev_fn(group.get("symbol") or ""))
|
||||
|
||||
try:
|
||||
order = cfg["market_add"](ex_sym, direction, float(amount), leverage)
|
||||
fill = float(
|
||||
cfg.get("resolve_fill_price", lambda o, s, p: p)(order, ex_sym, mark) or mark
|
||||
)
|
||||
replace = cfg.get("replace_tpsl")
|
||||
if callable(replace) and new_sl > 0 and tp0 > 0:
|
||||
mon = None
|
||||
if mon_id:
|
||||
row = conn.execute(
|
||||
"SELECT * FROM order_monitors WHERE id=?", (mon_id,)
|
||||
).fetchone()
|
||||
mon = _row_dict(row) if row else None
|
||||
try:
|
||||
replace(ex_sym, direction, new_sl, tp0, mon)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
fe = cfg.get("friendly_error")
|
||||
msg = fe(e) if callable(fe) else str(e)
|
||||
_notify_roll_fail(cfg, group, leg, mark, msg)
|
||||
return
|
||||
|
||||
oid = str(order.get("id") or "") if isinstance(order, dict) else ""
|
||||
cfg["replace_tpsl"](ex_sym, direction, sl, tp0, mon)
|
||||
conn.execute(
|
||||
"""UPDATE roll_legs SET status='filled', fill_price=?, amount=?, exchange_order_id=?,
|
||||
new_stop_loss=? WHERE id=? AND status='pending'""",
|
||||
(fill, float(amount), oid, sl, leg_id),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE roll_groups SET leg_count=?, current_stop_loss=?, updated_at=? WHERE id=?",
|
||||
(filled + 1, sl, _now(cfg), gid),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE order_monitors SET stop_loss=? WHERE id=? AND status='active'",
|
||||
(sl, mon["id"]),
|
||||
)
|
||||
|
||||
notify = cfg.get("send_wechat")
|
||||
if callable(notify):
|
||||
sym = group.get("symbol") or ""
|
||||
mode = leg.get("add_mode") or "限价"
|
||||
mode_lbl = leg.get("add_mode") or mode_label(mode)
|
||||
fmt = cfg.get("format_price")
|
||||
px_txt = fmt(sym, fill_px) if callable(fmt) else str(fill_px)
|
||||
sl_txt = fmt(sym, new_sl) if callable(fmt) else str(new_sl)
|
||||
px_txt = fmt(sym, fill) if callable(fmt) else str(fill)
|
||||
sl_txt = fmt(sym, sl) if callable(fmt) else str(sl)
|
||||
acct = _wechat_account(cfg)
|
||||
dir_txt = _wechat_dir(cfg, direction)
|
||||
notify(
|
||||
f"# ✅ {sym} 滚仓限价已成交\n"
|
||||
f"# ✅ {sym} 滚仓触价成交\n"
|
||||
f"**账户:{acct}**\n"
|
||||
f"- 方式:{mode}|{dir_txt}\n"
|
||||
f"- 成交价:{px_txt}|新止损:{sl_txt}\n"
|
||||
f"- 交易所止损已尝试同步(止盈仍为首仓)\n"
|
||||
f"- 方式:{mode_lbl}|{dir_txt}\n"
|
||||
f"- 成交价:{px_txt}|张数:{amount}\n"
|
||||
f"- 新止损:{sl_txt}(止盈仍为首仓)\n"
|
||||
)
|
||||
|
||||
|
||||
def _invalidate_roll_leg(
|
||||
conn,
|
||||
cfg: dict,
|
||||
group: dict,
|
||||
leg: dict,
|
||||
mark: float,
|
||||
*,
|
||||
reason: str = "",
|
||||
) -> None:
|
||||
leg_id = int(leg["id"])
|
||||
cur = conn.execute("SELECT status FROM roll_legs WHERE id=?", (leg_id,)).fetchone()
|
||||
if not cur or (cur[0] or "").strip().lower() in ("invalidated", "filled", "cancelled"):
|
||||
return
|
||||
_cancel_roll_leg_order(cfg, group, leg)
|
||||
conn.execute(
|
||||
"UPDATE roll_legs SET status='invalidated' WHERE id=? AND status='pending'",
|
||||
(leg_id,),
|
||||
)
|
||||
_send_roll_invalidate_wechat(cfg, group, leg, mark, reason=reason)
|
||||
|
||||
|
||||
def _notify_roll_fail(cfg: dict, group: dict, leg: dict, mark: float, reason: str) -> None:
|
||||
notify = cfg.get("send_wechat")
|
||||
if not callable(notify):
|
||||
return
|
||||
sym = group.get("symbol") or ""
|
||||
mode = leg.get("add_mode") or "滚仓"
|
||||
acct = _wechat_account(cfg)
|
||||
notify(
|
||||
f"# ❌ {sym} 滚仓触价成交失败\n"
|
||||
f"**账户:{acct}**\n"
|
||||
f"- 方式:{mode}\n"
|
||||
f"- 原因:{reason}\n"
|
||||
)
|
||||
|
||||
|
||||
def _send_roll_invalidate_wechat(
|
||||
cfg: dict, group: dict, leg: dict, mark: float
|
||||
cfg: dict, group: dict, leg: dict, mark: float, *, reason: str = ""
|
||||
) -> None:
|
||||
notify = cfg.get("send_wechat")
|
||||
if not callable(notify):
|
||||
return
|
||||
sym = group.get("symbol") or ""
|
||||
direction = (group.get("direction") or "long").strip().lower()
|
||||
mode = leg.get("add_mode") or "斐波限价"
|
||||
mode = leg.get("add_mode") or "滚仓监控"
|
||||
fmt = cfg.get("format_price")
|
||||
mark_txt = fmt(sym, mark) if callable(fmt) else str(mark)
|
||||
acct = _wechat_account(cfg)
|
||||
dir_txt = _wechat_dir(cfg, direction)
|
||||
detail = reason or "条件不满足"
|
||||
notify(
|
||||
f"# ⚠️ {sym} 滚仓斐波挂单失效\n"
|
||||
f"# ⚠️ {sym} 滚仓监控失效\n"
|
||||
f"**账户:{acct}**\n"
|
||||
f"- 方式:{mode}|{dir_txt}\n"
|
||||
f"- 标记价 {mark_txt} 已触达止盈侧(未成交),已撤限价加仓单\n"
|
||||
f"- 本条滚仓腿已结案,可继续下一档或重新挂单\n"
|
||||
f"- 标记价 {mark_txt}|{detail}\n"
|
||||
f"- 本条监控已结案,可重新提交\n"
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user