Fix roll first-lots display and make market add use pending orders.
Store initial_lots on roll groups, submit market roll as CTP pending legs with cancel closing empty groups, and backfill first-lots for existing active groups. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -130,6 +130,8 @@ ROLL_LEG_EXTRA_COLUMNS = (
|
|||||||
"ALTER TABLE roll_legs ADD COLUMN last_mark_price REAL",
|
"ALTER TABLE roll_legs ADD COLUMN last_mark_price REAL",
|
||||||
"ALTER TABLE roll_legs ADD COLUMN invalidated_reason TEXT",
|
"ALTER TABLE roll_legs ADD COLUMN invalidated_reason TEXT",
|
||||||
"ALTER TABLE roll_legs ADD COLUMN capital_snapshot REAL",
|
"ALTER TABLE roll_legs ADD COLUMN capital_snapshot REAL",
|
||||||
|
"ALTER TABLE roll_legs ADD COLUMN vt_order_id TEXT",
|
||||||
|
"ALTER TABLE roll_groups ADD COLUMN initial_lots INTEGER",
|
||||||
"ALTER TABLE trade_order_monitors ADD COLUMN risk_percent REAL",
|
"ALTER TABLE trade_order_monitors ADD COLUMN risk_percent REAL",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,25 @@ def roll_sync_after_external_close(conn, *, monitor_id: int) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def close_roll_group_if_idle(conn, gid: int) -> None:
|
||||||
|
"""无已成交腿且无 pending 腿时关闭滚仓组(如市价挂单撤单后)。"""
|
||||||
|
gid = int(gid)
|
||||||
|
filled = int(conn.execute(
|
||||||
|
"SELECT COUNT(*) AS n FROM roll_legs WHERE roll_group_id=? AND status=?",
|
||||||
|
(gid, LEG_STATUS_FILLED),
|
||||||
|
).fetchone()["n"] or 0)
|
||||||
|
pending = int(conn.execute(
|
||||||
|
"SELECT COUNT(*) AS n FROM roll_legs WHERE roll_group_id=? AND status=?",
|
||||||
|
(gid, LEG_STATUS_PENDING),
|
||||||
|
).fetchone()["n"] or 0)
|
||||||
|
if filled > 0 or pending > 0:
|
||||||
|
return
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE roll_groups SET status='closed', updated_at=? WHERE id=? AND status='active'",
|
||||||
|
(_now(), gid),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def cancel_roll_leg(conn, leg_id: int) -> tuple[bool, str]:
|
def cancel_roll_leg(conn, leg_id: int) -> tuple[bool, str]:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT * FROM roll_legs WHERE id=? AND status=?",
|
"SELECT * FROM roll_legs WHERE id=? AND status=?",
|
||||||
@@ -57,10 +76,12 @@ def cancel_roll_leg(conn, leg_id: int) -> tuple[bool, str]:
|
|||||||
).fetchone()
|
).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
return False, "仅可删除监控中的腿"
|
return False, "仅可删除监控中的腿"
|
||||||
|
leg = dict(row)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE roll_legs SET status=? WHERE id=?",
|
"UPDATE roll_legs SET status=? WHERE id=?",
|
||||||
(LEG_STATUS_CANCELLED, int(leg_id)),
|
(LEG_STATUS_CANCELLED, int(leg_id)),
|
||||||
)
|
)
|
||||||
|
close_roll_group_if_idle(conn, int(leg["roll_group_id"]))
|
||||||
return True, "已删除"
|
return True, "已删除"
|
||||||
|
|
||||||
|
|
||||||
@@ -89,6 +110,9 @@ def check_roll_monitors(
|
|||||||
).fetchall()
|
).fetchall()
|
||||||
for raw in rows:
|
for raw in rows:
|
||||||
leg = dict(raw)
|
leg = dict(raw)
|
||||||
|
mode = (leg.get("add_mode") or "").strip().lower()
|
||||||
|
if mode == ADD_MODE_MARKET:
|
||||||
|
continue
|
||||||
if (leg.get("mon_status") or "").strip().lower() != "active":
|
if (leg.get("mon_status") or "").strip().lower() != "active":
|
||||||
_invalidate_leg(conn, leg, "监控已结束")
|
_invalidate_leg(conn, leg, "监控已结束")
|
||||||
continue
|
continue
|
||||||
|
|||||||
+354
-94
@@ -93,6 +93,7 @@ from strategy.strategy_roll_lib import (
|
|||||||
from strategy.strategy_roll_monitor_lib import (
|
from strategy.strategy_roll_monitor_lib import (
|
||||||
cancel_roll_leg,
|
cancel_roll_leg,
|
||||||
check_roll_monitors,
|
check_roll_monitors,
|
||||||
|
close_roll_group_if_idle,
|
||||||
roll_sync_after_external_close,
|
roll_sync_after_external_close,
|
||||||
)
|
)
|
||||||
from strategy.strategy_snapshot_lib import list_snapshots, save_snapshot
|
from strategy.strategy_snapshot_lib import list_snapshots, save_snapshot
|
||||||
@@ -1802,7 +1803,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _reconcile_pending(conn, mode: str, *, capital: float = 0.0) -> dict[str, int]:
|
def _reconcile_pending(conn, mode: str, *, capital: float = 0.0) -> dict[str, int]:
|
||||||
return reconcile_pending_orders(
|
stats = reconcile_pending_orders(
|
||||||
conn,
|
conn,
|
||||||
mode,
|
mode,
|
||||||
match_symbol_fn=_match_ctp_symbol,
|
match_symbol_fn=_match_ctp_symbol,
|
||||||
@@ -1811,6 +1812,310 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
list_positions_fn=_ctp_positions,
|
list_positions_fn=_ctp_positions,
|
||||||
timeout_sec=get_pending_order_timeout_sec(get_setting),
|
timeout_sec=get_pending_order_timeout_sec(get_setting),
|
||||||
)
|
)
|
||||||
|
roll_stats = _reconcile_roll_market_orders(conn, mode, capital=capital)
|
||||||
|
if roll_stats:
|
||||||
|
stats = {**stats, **{f"roll_{k}": v for k, v in roll_stats.items()}}
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def _roll_insert_group(
|
||||||
|
conn,
|
||||||
|
*,
|
||||||
|
mon: dict,
|
||||||
|
preview: dict,
|
||||||
|
initial_lots: int,
|
||||||
|
leg_count: int = 0,
|
||||||
|
) -> int:
|
||||||
|
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
risk_budget = float(preview.get("risk_budget") or get_fixed_amount(get_setting))
|
||||||
|
cur = conn.execute(
|
||||||
|
"""INSERT INTO roll_groups (
|
||||||
|
order_monitor_id, symbol, direction, initial_take_profit, initial_stop_loss,
|
||||||
|
current_stop_loss, risk_percent, leg_count, initial_lots, status, created_at, updated_at
|
||||||
|
) VALUES (?,?,?,?,?,?,?,?,?,'active',?,?) RETURNING id""",
|
||||||
|
(
|
||||||
|
int(mon["id"]), mon["symbol"], mon["direction"], mon["take_profit"], mon["stop_loss"],
|
||||||
|
preview["new_stop_loss"], risk_budget, int(leg_count), int(initial_lots), now, now,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
return int(row["id"] if isinstance(row, dict) else row[0])
|
||||||
|
|
||||||
|
def _finalize_roll_fill(
|
||||||
|
conn,
|
||||||
|
*,
|
||||||
|
mon: dict,
|
||||||
|
preview: dict,
|
||||||
|
add_mode: str,
|
||||||
|
pending_leg_id: Optional[int] = None,
|
||||||
|
fill_price: Optional[float] = None,
|
||||||
|
) -> tuple[bool, str]:
|
||||||
|
sym = mon["symbol"]
|
||||||
|
mon_id = int(mon["id"])
|
||||||
|
mode = get_trading_mode(get_setting)
|
||||||
|
price = float(fill_price if fill_price is not None else preview["add_price"])
|
||||||
|
new_lots = int(mon["lots"]) + int(preview["add_lots"])
|
||||||
|
new_avg = preview["avg_entry_after"]
|
||||||
|
new_sl = preview["new_stop_loss"]
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE trade_order_monitors SET lots=?, entry_price=?, stop_loss=? WHERE id=?",
|
||||||
|
(new_lots, new_avg, new_sl, mon_id),
|
||||||
|
)
|
||||||
|
grp = _roll_group_for_monitor(conn, mon_id)
|
||||||
|
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
if grp:
|
||||||
|
gid = int(grp["id"])
|
||||||
|
leg_n = int(grp["leg_count"] or 0) + 1
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE roll_groups SET leg_count=?, current_stop_loss=?, updated_at=? WHERE id=?",
|
||||||
|
(leg_n, new_sl, now, gid),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
gid = _roll_insert_group(
|
||||||
|
conn,
|
||||||
|
mon=mon,
|
||||||
|
preview=preview,
|
||||||
|
initial_lots=max(1, int(mon["lots"]) - int(preview["add_lots"])),
|
||||||
|
leg_count=1,
|
||||||
|
)
|
||||||
|
leg_n = 1
|
||||||
|
if pending_leg_id:
|
||||||
|
conn.execute(
|
||||||
|
"""UPDATE roll_legs SET status=?, fill_price=?, lots=?, new_stop_loss=?, created_at=?
|
||||||
|
WHERE id=?""",
|
||||||
|
(
|
||||||
|
LEG_STATUS_FILLED, price, int(preview["add_lots"]), new_sl, now,
|
||||||
|
int(pending_leg_id),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO roll_legs (
|
||||||
|
roll_group_id, leg_index, add_mode, fill_price, lots, new_stop_loss,
|
||||||
|
status, created_at, limit_price, breakthrough_price, last_mark_price, capital_snapshot
|
||||||
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||||
|
(
|
||||||
|
gid, leg_n, add_mode, price, int(preview["add_lots"]), new_sl,
|
||||||
|
LEG_STATUS_FILLED, now,
|
||||||
|
preview.get("limit_price"), preview.get("breakthrough_price"),
|
||||||
|
preview.get("mark_price"), _capital(conn),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
send_wechat_msg(
|
||||||
|
f"滚仓成交 {sym} {add_mode_label(add_mode)} +{preview['add_lots']}手 "
|
||||||
|
f"新止损 {new_sl} 合计 {new_lots}手"
|
||||||
|
)
|
||||||
|
_schedule_roll_entry_sync(mon_id, sym, mon["direction"], mode)
|
||||||
|
return True, "成交"
|
||||||
|
|
||||||
|
def _commit_roll_fill(
|
||||||
|
conn,
|
||||||
|
*,
|
||||||
|
mon: dict,
|
||||||
|
preview: dict,
|
||||||
|
add_mode: str,
|
||||||
|
mode: str,
|
||||||
|
pending_leg_id: Optional[int] = None,
|
||||||
|
) -> tuple[bool, str]:
|
||||||
|
sym = mon["symbol"]
|
||||||
|
price = float(preview["add_price"])
|
||||||
|
try:
|
||||||
|
execute_order(
|
||||||
|
conn, mode=mode, offset="open", symbol=sym,
|
||||||
|
direction=mon["direction"], lots=int(preview["add_lots"]), price=price,
|
||||||
|
settings=_settings_dict(),
|
||||||
|
order_type="market",
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
return False, str(exc)
|
||||||
|
return _finalize_roll_fill(
|
||||||
|
conn,
|
||||||
|
mon=mon,
|
||||||
|
preview=preview,
|
||||||
|
add_mode=add_mode,
|
||||||
|
pending_leg_id=pending_leg_id,
|
||||||
|
fill_price=price,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _submit_roll_market_order(
|
||||||
|
conn,
|
||||||
|
*,
|
||||||
|
mon: dict,
|
||||||
|
preview: dict,
|
||||||
|
mode: str,
|
||||||
|
) -> tuple[bool, str]:
|
||||||
|
mon_id = int(mon["id"])
|
||||||
|
initial_lots = int(mon.get("lots") or 0)
|
||||||
|
if initial_lots <= 0:
|
||||||
|
return False, "持仓手数为 0"
|
||||||
|
grp = _roll_group_for_monitor(conn, mon_id)
|
||||||
|
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
if grp:
|
||||||
|
gid = int(grp["id"])
|
||||||
|
else:
|
||||||
|
gid = _roll_insert_group(
|
||||||
|
conn, mon=mon, preview=preview, initial_lots=initial_lots, leg_count=0,
|
||||||
|
)
|
||||||
|
pending_n = conn.execute(
|
||||||
|
"SELECT COUNT(*) AS n FROM roll_legs WHERE roll_group_id=? AND status=?",
|
||||||
|
(gid, LEG_STATUS_PENDING),
|
||||||
|
).fetchone()["n"]
|
||||||
|
if int(pending_n or 0) > 0:
|
||||||
|
return False, "已有监控中的加仓腿"
|
||||||
|
try:
|
||||||
|
result = execute_order(
|
||||||
|
conn,
|
||||||
|
mode=mode,
|
||||||
|
offset="open",
|
||||||
|
symbol=mon["symbol"],
|
||||||
|
direction=mon["direction"],
|
||||||
|
lots=int(preview["add_lots"]),
|
||||||
|
price=float(preview["add_price"]),
|
||||||
|
settings=_settings_dict(),
|
||||||
|
order_type="market",
|
||||||
|
)
|
||||||
|
vt_order_id = str(result.get("order_id") or "")
|
||||||
|
except ValueError as exc:
|
||||||
|
close_roll_group_if_idle(conn, gid)
|
||||||
|
return False, str(exc)
|
||||||
|
leg_n = int(conn.execute(
|
||||||
|
"SELECT COUNT(*) AS n FROM roll_legs WHERE roll_group_id=? AND status=?",
|
||||||
|
(gid, LEG_STATUS_FILLED),
|
||||||
|
).fetchone()["n"]) + 1
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO roll_legs (
|
||||||
|
roll_group_id, leg_index, add_mode, lots, new_stop_loss, status, created_at,
|
||||||
|
limit_price, breakthrough_price, last_mark_price, capital_snapshot, vt_order_id
|
||||||
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||||
|
(
|
||||||
|
gid, leg_n, ADD_MODE_MARKET, int(preview["add_lots"]), preview["new_stop_loss"],
|
||||||
|
LEG_STATUS_PENDING, now,
|
||||||
|
preview.get("limit_price"), preview.get("breakthrough_price"),
|
||||||
|
preview.get("mark_price"), _capital(conn), vt_order_id or None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
_reconcile_roll_market_orders(conn, mode, capital=_capital(conn))
|
||||||
|
return True, "市价加仓委托已提交,成交后自动更新;撤单后滚仓组将消失"
|
||||||
|
|
||||||
|
def _reconcile_roll_market_orders(
|
||||||
|
conn, mode: str, *, capital: float = 0.0,
|
||||||
|
) -> dict[str, int]:
|
||||||
|
"""同步市价滚仓 pending 腿:成交→入账;撤单/拒单→删除腿并关闭空组。"""
|
||||||
|
from modules.trading.order_pending import PENDING_ORDER_SETTLE_GRACE_SEC
|
||||||
|
|
||||||
|
stats = {"filled": 0, "cancelled": 0}
|
||||||
|
if not ctp_status(mode).get("connected"):
|
||||||
|
return stats
|
||||||
|
rows = conn.execute(
|
||||||
|
"""SELECT l.*, g.order_monitor_id, g.symbol, g.direction, g.initial_take_profit,
|
||||||
|
g.leg_count AS group_leg_count,
|
||||||
|
m.lots AS mon_lots, m.entry_price AS mon_entry, m.take_profit AS mon_tp,
|
||||||
|
m.status AS mon_status
|
||||||
|
FROM roll_legs l
|
||||||
|
JOIN roll_groups g ON g.id = l.roll_group_id
|
||||||
|
JOIN trade_order_monitors m ON m.id = g.order_monitor_id
|
||||||
|
WHERE l.status=? AND l.add_mode=? AND g.status='active'""",
|
||||||
|
(LEG_STATUS_PENDING, ADD_MODE_MARKET),
|
||||||
|
).fetchall()
|
||||||
|
if not rows:
|
||||||
|
return stats
|
||||||
|
positions = list(_ctp_positions(mode, refresh_if_empty=False) or [])
|
||||||
|
try:
|
||||||
|
active_orders = {
|
||||||
|
str(k): o
|
||||||
|
for o in (ctp_list_active_orders(mode) or [])
|
||||||
|
for k in (o.get("order_id"), o.get("vt_order_id"))
|
||||||
|
if k
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
active_orders = {}
|
||||||
|
now_ts = time.time()
|
||||||
|
for raw in rows:
|
||||||
|
leg = dict(raw)
|
||||||
|
if (leg.get("mon_status") or "").strip().lower() != "active":
|
||||||
|
continue
|
||||||
|
sym = (leg.get("symbol") or "").strip()
|
||||||
|
direction = (leg.get("direction") or "long").strip().lower()
|
||||||
|
mon = {
|
||||||
|
"id": leg["order_monitor_id"],
|
||||||
|
"symbol": sym,
|
||||||
|
"direction": direction,
|
||||||
|
"lots": leg["mon_lots"],
|
||||||
|
"entry_price": leg["mon_entry"],
|
||||||
|
"take_profit": leg["mon_tp"] or leg["initial_take_profit"],
|
||||||
|
"stop_loss": leg.get("new_stop_loss"),
|
||||||
|
}
|
||||||
|
leg_lots = int(leg.get("lots") or 0)
|
||||||
|
mon_lots = int(leg.get("mon_lots") or 0)
|
||||||
|
pos = None
|
||||||
|
for p in positions:
|
||||||
|
if int(p.get("lots") or 0) <= 0:
|
||||||
|
continue
|
||||||
|
if (p.get("direction") or "long").strip().lower() != direction:
|
||||||
|
continue
|
||||||
|
if _match_ctp_symbol(p.get("symbol") or "", sym):
|
||||||
|
pos = p
|
||||||
|
break
|
||||||
|
pos_lots = int(pos.get("lots") or 0) if pos else mon_lots
|
||||||
|
vt_oid = (leg.get("vt_order_id") or "").strip()
|
||||||
|
if pos_lots >= mon_lots + leg_lots and leg_lots > 0:
|
||||||
|
mark = _roll_mark_price(sym, mon, mode, allow_ctp=True)
|
||||||
|
preview, err = preview_roll(
|
||||||
|
direction=direction,
|
||||||
|
symbol=sym,
|
||||||
|
qty_existing=float(mon_lots),
|
||||||
|
entry_existing=float(mon.get("entry_price") or 0),
|
||||||
|
initial_take_profit=float(mon.get("take_profit") or 0),
|
||||||
|
add_mode=ADD_MODE_MARKET,
|
||||||
|
new_stop_loss=float(leg.get("new_stop_loss") or 0),
|
||||||
|
risk_budget=float(leg.get("capital_snapshot") or get_fixed_amount(get_setting)),
|
||||||
|
mult=int(get_contract_spec(sym).get("mult") or 1),
|
||||||
|
mark_price=mark,
|
||||||
|
add_price=mark,
|
||||||
|
legs_done=int(leg.get("group_leg_count") or 0),
|
||||||
|
)
|
||||||
|
if err or not preview:
|
||||||
|
continue
|
||||||
|
preview["add_lots"] = leg_lots
|
||||||
|
ok, _ = _finalize_roll_fill(
|
||||||
|
conn,
|
||||||
|
mon=mon,
|
||||||
|
preview=preview,
|
||||||
|
add_mode=ADD_MODE_MARKET,
|
||||||
|
pending_leg_id=int(leg["id"]),
|
||||||
|
fill_price=mark,
|
||||||
|
)
|
||||||
|
if ok:
|
||||||
|
stats["filled"] += 1
|
||||||
|
continue
|
||||||
|
if vt_oid and vt_oid in active_orders:
|
||||||
|
continue
|
||||||
|
created = (leg.get("created_at") or "").strip()
|
||||||
|
age = 9999.0
|
||||||
|
if created:
|
||||||
|
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
|
||||||
|
try:
|
||||||
|
age = now_ts - datetime.strptime(created[:19], fmt).timestamp()
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if vt_oid and age < PENDING_ORDER_SETTLE_GRACE_SEC:
|
||||||
|
continue
|
||||||
|
if vt_oid:
|
||||||
|
try:
|
||||||
|
ctp_cancel_order(mode, vt_oid)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE roll_legs SET status=? WHERE id=?",
|
||||||
|
(LEG_STATUS_CANCELLED, int(leg["id"])),
|
||||||
|
)
|
||||||
|
close_roll_group_if_idle(conn, int(leg["roll_group_id"]))
|
||||||
|
stats["cancelled"] += 1
|
||||||
|
if stats["filled"] or stats["cancelled"]:
|
||||||
|
conn.commit()
|
||||||
|
return stats
|
||||||
|
|
||||||
def _build_active_orders(
|
def _build_active_orders(
|
||||||
conn,
|
conn,
|
||||||
@@ -3043,6 +3348,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
gid = int(out.get("id") or 0)
|
gid = int(out.get("id") or 0)
|
||||||
filled_add_lots = int(filled_map.get(gid) or 0)
|
filled_add_lots = int(filled_map.get(gid) or 0)
|
||||||
out["add_lots_filled"] = filled_add_lots
|
out["add_lots_filled"] = filled_add_lots
|
||||||
|
stored_initial = int(out.get("initial_lots") or 0)
|
||||||
|
if stored_initial > 0:
|
||||||
|
out["first_lots"] = stored_initial
|
||||||
|
elif int(lots) > 0 and filled_add_lots > 0 and int(lots) <= filled_add_lots:
|
||||||
|
out["first_lots"] = int(lots)
|
||||||
|
else:
|
||||||
out["first_lots"] = max(0, int(lots) - filled_add_lots)
|
out["first_lots"] = max(0, int(lots) - filled_add_lots)
|
||||||
out["total_lots"] = int(lots)
|
out["total_lots"] = int(lots)
|
||||||
out["avg_entry"] = round(entry, 4) if entry > 0 else None
|
out["avg_entry"] = round(entry, 4) if entry > 0 else None
|
||||||
@@ -3239,6 +3550,27 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
capital = _capital(conn)
|
capital = _capital(conn)
|
||||||
mode = get_trading_mode(get_setting)
|
mode = get_trading_mode(get_setting)
|
||||||
_ensure_strategy_monitors(conn, mode)
|
_ensure_strategy_monitors(conn, mode)
|
||||||
|
for grow in conn.execute(
|
||||||
|
"SELECT id, order_monitor_id, initial_lots FROM roll_groups WHERE status='active'"
|
||||||
|
).fetchall():
|
||||||
|
gfix = dict(grow)
|
||||||
|
if int(gfix.get("initial_lots") or 0) > 0:
|
||||||
|
continue
|
||||||
|
mon_row = conn.execute(
|
||||||
|
"SELECT lots FROM trade_order_monitors WHERE id=?",
|
||||||
|
(int(gfix["order_monitor_id"]),),
|
||||||
|
).fetchone()
|
||||||
|
if not mon_row:
|
||||||
|
continue
|
||||||
|
gid = int(gfix["id"])
|
||||||
|
filled = _roll_filled_lots_map(conn, [gid]).get(gid, 0)
|
||||||
|
lots = int(mon_row["lots"] or 0)
|
||||||
|
initial = lots if lots > 0 and filled > 0 and lots <= filled else max(1, lots - filled)
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE roll_groups SET initial_lots=? WHERE id=?",
|
||||||
|
(initial, gid),
|
||||||
|
)
|
||||||
|
commit_retry(conn)
|
||||||
active_trend = conn.execute(
|
active_trend = conn.execute(
|
||||||
"SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC LIMIT 1"
|
"SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC LIMIT 1"
|
||||||
).fetchone()
|
).fetchone()
|
||||||
@@ -4085,87 +4417,6 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
return None, merr
|
return None, merr
|
||||||
return preview, None
|
return preview, None
|
||||||
|
|
||||||
def _commit_roll_fill(
|
|
||||||
conn,
|
|
||||||
*,
|
|
||||||
mon: dict,
|
|
||||||
preview: dict,
|
|
||||||
add_mode: str,
|
|
||||||
mode: str,
|
|
||||||
pending_leg_id: Optional[int] = None,
|
|
||||||
) -> tuple[bool, str]:
|
|
||||||
sym = mon["symbol"]
|
|
||||||
mon_id = int(mon["id"])
|
|
||||||
price = float(preview["add_price"])
|
|
||||||
try:
|
|
||||||
execute_order(
|
|
||||||
conn, mode=mode, offset="open", symbol=sym,
|
|
||||||
direction=mon["direction"], lots=int(preview["add_lots"]), price=price,
|
|
||||||
settings=_settings_dict(),
|
|
||||||
)
|
|
||||||
except ValueError as exc:
|
|
||||||
return False, str(exc)
|
|
||||||
new_lots = int(mon["lots"]) + int(preview["add_lots"])
|
|
||||||
new_avg = preview["avg_entry_after"]
|
|
||||||
new_sl = preview["new_stop_loss"]
|
|
||||||
conn.execute(
|
|
||||||
"UPDATE trade_order_monitors SET lots=?, entry_price=?, stop_loss=? WHERE id=?",
|
|
||||||
(new_lots, new_avg, new_sl, mon_id),
|
|
||||||
)
|
|
||||||
grp = _roll_group_for_monitor(conn, mon_id)
|
|
||||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
risk_budget = float(preview.get("risk_budget") or get_fixed_amount(get_setting))
|
|
||||||
if grp:
|
|
||||||
gid = int(grp["id"])
|
|
||||||
leg_n = int(grp["leg_count"] or 0) + 1
|
|
||||||
conn.execute(
|
|
||||||
"UPDATE roll_groups SET leg_count=?, current_stop_loss=?, updated_at=? WHERE id=?",
|
|
||||||
(leg_n, new_sl, now, gid),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cur = conn.execute(
|
|
||||||
"""INSERT INTO roll_groups (
|
|
||||||
order_monitor_id, symbol, direction, initial_take_profit, initial_stop_loss,
|
|
||||||
current_stop_loss, risk_percent, leg_count, status, created_at, updated_at
|
|
||||||
) VALUES (?,?,?,?,?,?,?,1,'active',?,?) RETURNING id""",
|
|
||||||
(
|
|
||||||
mon_id, sym, mon["direction"], mon["take_profit"], mon["stop_loss"],
|
|
||||||
new_sl, risk_budget, now, now,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
row = cur.fetchone()
|
|
||||||
gid = int(row["id"] if isinstance(row, dict) else row[0])
|
|
||||||
leg_n = 1
|
|
||||||
if pending_leg_id:
|
|
||||||
conn.execute(
|
|
||||||
"""UPDATE roll_legs SET status=?, fill_price=?, lots=?, new_stop_loss=?, created_at=?
|
|
||||||
WHERE id=?""",
|
|
||||||
(
|
|
||||||
LEG_STATUS_FILLED, price, int(preview["add_lots"]), new_sl, now,
|
|
||||||
int(pending_leg_id),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
conn.execute(
|
|
||||||
"""INSERT INTO roll_legs (
|
|
||||||
roll_group_id, leg_index, add_mode, fill_price, lots, new_stop_loss,
|
|
||||||
status, created_at, limit_price, breakthrough_price, last_mark_price, capital_snapshot
|
|
||||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
||||||
(
|
|
||||||
gid, leg_n, add_mode, price, int(preview["add_lots"]), new_sl,
|
|
||||||
LEG_STATUS_FILLED, now,
|
|
||||||
preview.get("limit_price"), preview.get("breakthrough_price"),
|
|
||||||
preview.get("mark_price"), _capital(conn),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
send_wechat_msg(
|
|
||||||
f"滚仓成交 {sym} {add_mode_label(add_mode)} +{preview['add_lots']}手 "
|
|
||||||
f"新止损 {new_sl} 合计 {new_lots}手"
|
|
||||||
)
|
|
||||||
_schedule_roll_entry_sync(mon_id, sym, mon["direction"], mode)
|
|
||||||
return True, "成交"
|
|
||||||
|
|
||||||
def _schedule_roll_entry_sync(
|
def _schedule_roll_entry_sync(
|
||||||
mon_id: int, sym: str, direction: str, mode: str,
|
mon_id: int, sym: str, direction: str, mode: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -4209,25 +4460,16 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
add_mode: str,
|
add_mode: str,
|
||||||
) -> tuple[bool, str]:
|
) -> tuple[bool, str]:
|
||||||
mon_id = int(mon["id"])
|
mon_id = int(mon["id"])
|
||||||
|
initial_lots = int(mon.get("lots") or 0)
|
||||||
grp = _roll_group_for_monitor(conn, mon_id)
|
grp = _roll_group_for_monitor(conn, mon_id)
|
||||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
capital = _capital(conn)
|
capital = _capital(conn)
|
||||||
risk_budget = float(preview.get("risk_budget") or get_fixed_amount(get_setting))
|
|
||||||
if grp:
|
if grp:
|
||||||
gid = int(grp["id"])
|
gid = int(grp["id"])
|
||||||
else:
|
else:
|
||||||
cur = conn.execute(
|
gid = _roll_insert_group(
|
||||||
"""INSERT INTO roll_groups (
|
conn, mon=mon, preview=preview, initial_lots=initial_lots, leg_count=0,
|
||||||
order_monitor_id, symbol, direction, initial_take_profit, initial_stop_loss,
|
|
||||||
current_stop_loss, risk_percent, leg_count, status, created_at, updated_at
|
|
||||||
) VALUES (?,?,?,?,?,?,?,0,'active',?,?) RETURNING id""",
|
|
||||||
(
|
|
||||||
mon_id, mon["symbol"], mon["direction"], mon["take_profit"], mon["stop_loss"],
|
|
||||||
preview["new_stop_loss"], risk_budget, now, now,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
row = cur.fetchone()
|
|
||||||
gid = int(row["id"] if isinstance(row, dict) else row[0])
|
|
||||||
leg_n = int(conn.execute(
|
leg_n = int(conn.execute(
|
||||||
"SELECT COUNT(*) AS n FROM roll_legs WHERE roll_group_id=? AND status=?",
|
"SELECT COUNT(*) AS n FROM roll_legs WHERE roll_group_id=? AND status=?",
|
||||||
(gid, LEG_STATUS_FILLED),
|
(gid, LEG_STATUS_FILLED),
|
||||||
@@ -4403,6 +4645,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
if not _cached_ctp_status(mode).get("connected"):
|
if not _cached_ctp_status(mode).get("connected"):
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({"ok": False, "error": "请先连接 CTP"}), 400
|
return jsonify({"ok": False, "error": "请先连接 CTP"}), 400
|
||||||
|
if add_mode == ADD_MODE_MARKET:
|
||||||
|
ok, msg = _submit_roll_market_order(conn, mon=mon_d, preview=preview, mode=mode)
|
||||||
|
conn.close()
|
||||||
|
if not ok:
|
||||||
|
return jsonify({"ok": False, "error": msg}), 400
|
||||||
|
return jsonify({"ok": True, "message": msg, "pending": True})
|
||||||
ok, msg = _commit_roll_fill(
|
ok, msg = _commit_roll_fill(
|
||||||
conn, mon=mon_d, preview=preview, add_mode=add_mode, mode=mode,
|
conn, mon=mon_d, preview=preview, add_mode=add_mode, mode=mode,
|
||||||
)
|
)
|
||||||
@@ -4416,6 +4664,18 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
def api_roll_cancel(leg_id: int):
|
def api_roll_cancel(leg_id: int):
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
init_strategy_tables(conn)
|
init_strategy_tables(conn)
|
||||||
|
mode = get_trading_mode(get_setting)
|
||||||
|
leg = conn.execute(
|
||||||
|
"SELECT * FROM roll_legs WHERE id=? AND status=?",
|
||||||
|
(int(leg_id), LEG_STATUS_PENDING),
|
||||||
|
).fetchone()
|
||||||
|
if leg:
|
||||||
|
vt_oid = (dict(leg).get("vt_order_id") or "").strip()
|
||||||
|
if vt_oid and ctp_status(mode).get("connected"):
|
||||||
|
try:
|
||||||
|
ctp_cancel_order(mode, vt_oid)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("roll leg cancel order: %s", exc)
|
||||||
ok, msg = cancel_roll_leg(conn, leg_id)
|
ok, msg = cancel_roll_leg(conn, leg_id)
|
||||||
if ok:
|
if ok:
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
}
|
}
|
||||||
if (execHint) execHint.hidden = false;
|
if (execHint) execHint.hidden = false;
|
||||||
if (btnExec) {
|
if (btnExec) {
|
||||||
btnExec.textContent = mode === 'market' ? '执行滚仓' : '提交监控';
|
btnExec.textContent = mode === 'market' ? '提交委托' : '提交监控';
|
||||||
}
|
}
|
||||||
if (btnPreview) {
|
if (btnPreview) {
|
||||||
btnPreview.disabled = false;
|
btnPreview.disabled = false;
|
||||||
@@ -257,21 +257,22 @@
|
|||||||
btnRollE.addEventListener('click', function () {
|
btnRollE.addEventListener('click', function () {
|
||||||
var payload = rollPayload || formData(rollForm);
|
var payload = rollPayload || formData(rollForm);
|
||||||
var mode = (payload.add_mode || 'market');
|
var mode = (payload.add_mode || 'market');
|
||||||
if (mode === 'market') {
|
if (mode === 'market' && !inTradingSession) {
|
||||||
if (!inTradingSession) {
|
|
||||||
alert('休盘期间请切换为「突破加仓」后提交监控');
|
alert('休盘期间请切换为「突破加仓」后提交监控');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!confirm('确认执行市价滚仓?')) return;
|
var confirmMsg = mode === 'market'
|
||||||
startRollCountdown(btnRollE, payload);
|
? '确认提交市价加仓委托?可在「正在滚仓」中撤单。'
|
||||||
return;
|
: '确认提交突破加仓监控?';
|
||||||
}
|
if (!confirm(confirmMsg)) return;
|
||||||
btnRollE.disabled = true;
|
btnRollE.disabled = true;
|
||||||
btnRollE.textContent = '提交中…';
|
btnRollE.textContent = '提交中…';
|
||||||
jsonPost('/api/strategy/roll/execute', payload).then(function (d) {
|
jsonPost('/api/strategy/roll/execute', payload).then(function (d) {
|
||||||
if (!d.ok) { alert(d.error || '失败'); return; }
|
if (!d.ok) { alert(d.error || '失败'); return; }
|
||||||
alert(d.message || '已提交监控');
|
alert(d.message || (mode === 'market' ? '委托已提交' : '已提交监控'));
|
||||||
location.reload();
|
location.reload();
|
||||||
|
}).catch(function () {
|
||||||
|
alert('提交失败,请稍后重试');
|
||||||
}).finally(function () {
|
}).finally(function () {
|
||||||
btnRollE.disabled = false;
|
btnRollE.disabled = false;
|
||||||
syncRollModeUi();
|
syncRollModeUi();
|
||||||
|
|||||||
@@ -139,7 +139,7 @@
|
|||||||
<button type="button" class="btn-primary" id="btn-roll-exec" hidden {% if not roll_allowed %}disabled{% endif %}>执行滚仓</button>
|
<button type="button" class="btn-primary" id="btn-roll-exec" hidden {% if not roll_allowed %}disabled{% endif %}>执行滚仓</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="roll-preview" class="strategy-preview" hidden></div>
|
<div id="roll-preview" class="strategy-preview" hidden></div>
|
||||||
<p class="hint" id="roll-exec-hint" hidden style="font-size:.75rem;margin-top:.45rem">市价加仓:须交易时段内确认,10 秒倒计时执行;突破加仓:休盘也可提交,开盘后再监控触价</p>
|
<p class="hint" id="roll-exec-hint" hidden style="font-size:.75rem;margin-top:.45rem">市价加仓:提交后进入「正在滚仓」挂单,成交后更新持仓;撤单后滚仓组消失。突破加仓:休盘也可提交,开盘触价后自动市价加仓。</p>
|
||||||
{% if not trading_session %}
|
{% if not trading_session %}
|
||||||
<p class="hint text-muted" id="roll-off-session-hint" style="font-size:.75rem;margin-top:.35rem">当前{{ session_clock.status_label or '休盘' }}:市价加仓须交易时段内执行;休盘可先预览,或选「突破加仓」提交监控。</p>
|
<p class="hint text-muted" id="roll-off-session-hint" style="font-size:.75rem;margin-top:.35rem">当前{{ session_clock.status_label or '休盘' }}:市价加仓须交易时段内执行;休盘可先预览,或选「突破加仓」提交监控。</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
Reference in New Issue
Block a user