Add futures roll strategy with breakout monitoring and fixed-amount sizing.
Replace percent-based risk with system fixed amount, support market/breakout add modes only, allow pending submission outside trading hours, and fix short breakout geometry plus route registration. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+386
-106
@@ -74,7 +74,23 @@ from risk.account_risk_lib import (
|
||||
trading_day_label,
|
||||
)
|
||||
from strategy.strategy_db import init_strategy_tables
|
||||
from strategy.strategy_roll_lib import avg_entry_after_add, preview_roll
|
||||
from strategy.strategy_roll_lib import (
|
||||
ADD_MODE_BREAKOUT,
|
||||
ADD_MODE_MARKET,
|
||||
FIB_MODES,
|
||||
LEG_STATUS_FILLED,
|
||||
LEG_STATUS_PENDING,
|
||||
PENDING_MODES,
|
||||
add_mode_label,
|
||||
avg_entry_after_add,
|
||||
preview_roll,
|
||||
roll_eligibility_error,
|
||||
)
|
||||
from strategy.strategy_roll_monitor_lib import (
|
||||
cancel_roll_leg,
|
||||
check_roll_monitors,
|
||||
roll_sync_after_external_close,
|
||||
)
|
||||
from strategy.strategy_snapshot_lib import list_snapshots, save_snapshot
|
||||
from strategy.strategy_trend_lib import (
|
||||
compute_trend_plan_futures,
|
||||
@@ -852,7 +868,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
"""UPDATE trade_order_monitors SET
|
||||
symbol=?, symbol_name=?, market_code=?, lots=?, entry_price=?,
|
||||
stop_loss=?, take_profit=?, initial_stop_loss=?, trailing_be=?, open_time=?,
|
||||
monitor_type=?, status=?, vt_order_id=?, order_price=?
|
||||
monitor_type=?, status=?, vt_order_id=?, order_price=?, risk_percent=COALESCE(risk_percent, ?)
|
||||
WHERE id=?""",
|
||||
(
|
||||
sym,
|
||||
@@ -869,6 +885,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
status_val,
|
||||
vt_val,
|
||||
order_px,
|
||||
get_risk_percent(get_setting),
|
||||
mid,
|
||||
),
|
||||
)
|
||||
@@ -883,8 +900,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
"""INSERT INTO trade_order_monitors (
|
||||
symbol, symbol_name, market_code, direction, lots, entry_price,
|
||||
stop_loss, take_profit, initial_stop_loss, trailing_be,
|
||||
open_time, monitor_type, status, vt_order_id, order_price
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
open_time, monitor_type, status, vt_order_id, order_price, risk_percent
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
sym,
|
||||
codes.get("name", sym),
|
||||
@@ -901,6 +918,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
status_val,
|
||||
vt_order_id,
|
||||
order_px,
|
||||
get_risk_percent(get_setting),
|
||||
),
|
||||
)
|
||||
mid = int(conn.execute("SELECT last_insert_rowid()").fetchone()[0])
|
||||
@@ -2083,22 +2101,73 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
return jsonify({"ok": False, "error": str(exc)}), 400
|
||||
|
||||
|
||||
def _roll_ui_modes():
|
||||
return frozenset({ADD_MODE_MARKET, ADD_MODE_BREAKOUT})
|
||||
|
||||
def _enrich_roll_group_row(row: dict) -> dict:
|
||||
out = dict(row)
|
||||
lots = float(out.get("mon_lots") or 0)
|
||||
entry = float(out.get("mon_entry") or 0)
|
||||
tp = float(out.get("mon_tp") or out.get("initial_take_profit") or 0)
|
||||
direction = (out.get("direction") or "long").strip().lower()
|
||||
sym = (out.get("symbol") or "").strip()
|
||||
mult = int(get_contract_spec(sym).get("mult") or 1) if sym else 1
|
||||
out["avg_entry"] = round(entry, 4) if entry > 0 else None
|
||||
if lots > 0 and entry > 0 and tp > 0:
|
||||
if direction == "long":
|
||||
out["reward_at_tp"] = round((tp - entry) * lots * mult, 2)
|
||||
else:
|
||||
out["reward_at_tp"] = round((entry - tp) * lots * mult, 2)
|
||||
else:
|
||||
out["reward_at_tp"] = None
|
||||
return out
|
||||
|
||||
def _roll_leg_trigger_price(leg: dict):
|
||||
for key in ("breakthrough_price", "limit_price", "fill_price"):
|
||||
val = leg.get(key)
|
||||
if val not in (None, "", 0):
|
||||
return val
|
||||
return None
|
||||
|
||||
@app.route("/strategy")
|
||||
@login_required
|
||||
@_nav("strategy")
|
||||
def strategy_page():
|
||||
conn = get_db()
|
||||
init_strategy_tables(conn)
|
||||
ensure_monitor_order_columns(conn)
|
||||
capital = _capital(conn)
|
||||
active_trend = conn.execute(
|
||||
"SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC LIMIT 1"
|
||||
).fetchone()
|
||||
monitors = conn.execute(
|
||||
monitors_raw = conn.execute(
|
||||
"SELECT * FROM trade_order_monitors WHERE status='active' ORDER BY id DESC"
|
||||
).fetchall()
|
||||
roll_groups = conn.execute(
|
||||
"SELECT * FROM roll_groups WHERE status='active' ORDER BY id DESC"
|
||||
"""SELECT g.*, m.symbol_name, m.lots AS mon_lots, m.entry_price AS mon_entry,
|
||||
m.take_profit AS mon_tp
|
||||
FROM roll_groups g
|
||||
LEFT JOIN trade_order_monitors m ON m.id = g.order_monitor_id
|
||||
WHERE g.status='active' ORDER BY g.id DESC"""
|
||||
).fetchall()
|
||||
roll_legs = conn.execute(
|
||||
"""SELECT l.*, g.symbol, g.direction, g.order_monitor_id
|
||||
FROM roll_legs l
|
||||
JOIN roll_groups g ON g.id = l.roll_group_id
|
||||
ORDER BY l.id DESC LIMIT 30"""
|
||||
).fetchall()
|
||||
sizing = get_sizing_mode(get_setting)
|
||||
roll_allowed = sizing == MODE_AMOUNT
|
||||
monitors = []
|
||||
for m in monitors_raw:
|
||||
row = dict(m)
|
||||
err = _roll_eligibility(conn, row)
|
||||
row["roll_eligible"] = roll_allowed and err is None
|
||||
if not roll_allowed:
|
||||
row["roll_block_reason"] = "仅固定金额(以损定仓)模式可滚仓"
|
||||
else:
|
||||
row["roll_block_reason"] = err or ""
|
||||
monitors.append(row)
|
||||
active_trend_row = dict(active_trend) if active_trend else None
|
||||
if active_trend_row:
|
||||
active_trend_row["period_label"] = trend_period_label(active_trend_row.get("period") or "15m")
|
||||
@@ -2106,12 +2175,25 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
return render_template(
|
||||
"strategy.html",
|
||||
capital=capital,
|
||||
risk_percent=get_risk_percent(get_setting),
|
||||
sizing_mode=get_sizing_mode(get_setting),
|
||||
fixed_amount=get_fixed_amount(get_setting),
|
||||
sizing_mode=sizing,
|
||||
sizing_mode_label=_sizing_mode_label(sizing),
|
||||
roll_allowed=roll_allowed,
|
||||
active_trend=active_trend_row,
|
||||
monitors=[dict(m) for m in monitors],
|
||||
roll_groups=[dict(g) for g in roll_groups],
|
||||
monitors=monitors,
|
||||
roll_groups=[_enrich_roll_group_row(dict(g)) for g in roll_groups],
|
||||
roll_legs=[dict(l) for l in roll_legs],
|
||||
trend_periods=trend_strategy_periods(),
|
||||
add_mode_labels={
|
||||
"market": "市价加仓",
|
||||
"breakout": "突破加仓",
|
||||
},
|
||||
roll_leg_status_labels={
|
||||
"pending": "监控中",
|
||||
"filled": "已成交",
|
||||
"cancelled": "已删除",
|
||||
"invalidated": "已失效",
|
||||
},
|
||||
)
|
||||
|
||||
@app.route("/strategy/records")
|
||||
@@ -2724,6 +2806,246 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
send_wechat_msg(f"趋势回调首仓 {sym} {plan['first_lots']}手")
|
||||
return jsonify({"ok": True, "plan_id": plan_id, "plan": plan})
|
||||
|
||||
def _roll_group_for_monitor(conn, monitor_id: int):
|
||||
return conn.execute(
|
||||
"SELECT * FROM roll_groups WHERE order_monitor_id=? AND status='active'",
|
||||
(int(monitor_id),),
|
||||
).fetchone()
|
||||
|
||||
def _roll_filled_legs(conn, monitor_id: int) -> int:
|
||||
grp = _roll_group_for_monitor(conn, monitor_id)
|
||||
if grp:
|
||||
return int(grp["leg_count"] or 0)
|
||||
return 0
|
||||
|
||||
def _roll_has_pending(conn, monitor_id: int) -> bool:
|
||||
grp = _roll_group_for_monitor(conn, monitor_id)
|
||||
if not grp:
|
||||
return False
|
||||
return bool(conn.execute(
|
||||
"SELECT 1 FROM roll_legs WHERE roll_group_id=? AND status=? LIMIT 1",
|
||||
(int(grp["id"]), LEG_STATUS_PENDING),
|
||||
).fetchone())
|
||||
|
||||
def _roll_eligibility(conn, mon: dict) -> Optional[str]:
|
||||
has_trend = bool(conn.execute(
|
||||
"SELECT 1 FROM trend_pullback_plans WHERE status='active' LIMIT 1",
|
||||
).fetchone())
|
||||
return roll_eligibility_error(
|
||||
sizing_mode=get_sizing_mode(get_setting),
|
||||
monitor=mon,
|
||||
has_active_trend=has_trend,
|
||||
legs_done=_roll_filled_legs(conn, int(mon["id"])),
|
||||
has_pending_leg=_roll_has_pending(conn, int(mon["id"])),
|
||||
)
|
||||
|
||||
def _roll_mark_price(sym: str, mon: dict, mode: str) -> float:
|
||||
mark = ctp_get_tick_price(mode, sym) if ctp_status(mode).get("connected") else None
|
||||
if mark and mark > 0:
|
||||
return float(mark)
|
||||
px = fetch_price(sym)
|
||||
if px and px > 0:
|
||||
return float(px)
|
||||
return float(mon.get("entry_price") or 0)
|
||||
|
||||
def _build_roll_preview(conn, d: dict, mon: dict, *, mode: str):
|
||||
sym = mon["symbol"]
|
||||
spec = get_contract_spec(sym)
|
||||
capital = _capital(conn)
|
||||
mark = _roll_mark_price(sym, mon, mode)
|
||||
add_mode = (d.get("add_mode") or ADD_MODE_MARKET).strip().lower()
|
||||
if add_mode in FIB_MODES:
|
||||
return None, "斐波加仓已停用,请选市价或突破"
|
||||
if add_mode not in _roll_ui_modes():
|
||||
return None, "仅支持市价加仓或突破加仓"
|
||||
risk_budget = get_fixed_amount(get_setting)
|
||||
legs_done = _roll_filled_legs(conn, int(mon["id"]))
|
||||
preview, err = preview_roll(
|
||||
direction=mon["direction"],
|
||||
symbol=sym,
|
||||
qty_existing=float(mon["lots"]),
|
||||
entry_existing=float(mon["entry_price"]),
|
||||
initial_take_profit=float(mon["take_profit"] or 0),
|
||||
add_mode=add_mode,
|
||||
new_stop_loss=float(d.get("new_stop_loss") or 0),
|
||||
risk_budget=risk_budget,
|
||||
mult=int(spec["mult"]),
|
||||
mark_price=mark,
|
||||
add_price=float(d.get("add_price") or 0) or mark,
|
||||
limit_price=d.get("limit_price"),
|
||||
breakthrough_price=d.get("breakthrough_price"),
|
||||
fib_upper=d.get("fib_upper"),
|
||||
fib_lower=d.get("fib_lower"),
|
||||
legs_done=legs_done,
|
||||
)
|
||||
if err:
|
||||
return None, err
|
||||
preview, merr = _apply_roll_margin_cap(
|
||||
preview, conn=conn, mode=mode, mon=dict(mon), capital=capital,
|
||||
)
|
||||
if merr:
|
||||
return None, merr
|
||||
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',?,?)""",
|
||||
(
|
||||
mon_id, sym, mon["direction"], mon["take_profit"], mon["stop_loss"],
|
||||
new_sl, risk_budget, now, now,
|
||||
),
|
||||
)
|
||||
gid = int(cur.lastrowid)
|
||||
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}手"
|
||||
)
|
||||
return True, "成交"
|
||||
|
||||
def _submit_roll_pending(
|
||||
conn,
|
||||
*,
|
||||
mon: dict,
|
||||
preview: dict,
|
||||
add_mode: str,
|
||||
) -> tuple[bool, str]:
|
||||
mon_id = int(mon["id"])
|
||||
grp = _roll_group_for_monitor(conn, mon_id)
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
capital = _capital(conn)
|
||||
risk_budget = float(preview.get("risk_budget") or get_fixed_amount(get_setting))
|
||||
if grp:
|
||||
gid = int(grp["id"])
|
||||
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 (?,?,?,?,?,?,?,0,'active',?,?)""",
|
||||
(
|
||||
mon_id, mon["symbol"], mon["direction"], mon["take_profit"], mon["stop_loss"],
|
||||
preview["new_stop_loss"], risk_budget, now, now,
|
||||
),
|
||||
)
|
||||
gid = int(cur.lastrowid)
|
||||
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
|
||||
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, "已有监控中的加仓腿"
|
||||
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
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
gid, leg_n, add_mode, 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.commit()
|
||||
return True, "已提交监控,触价后自动市价加仓"
|
||||
|
||||
def _fill_roll_leg_cb(mon: dict, grp: dict, leg: dict, preview: dict) -> tuple[bool, str]:
|
||||
conn = get_db()
|
||||
init_strategy_tables(conn)
|
||||
mode = get_trading_mode(get_setting)
|
||||
ok, msg = _commit_roll_fill(
|
||||
conn, mon=mon, preview=preview, add_mode=leg.get("add_mode") or ADD_MODE_MARKET,
|
||||
mode=mode, pending_leg_id=int(leg["id"]),
|
||||
)
|
||||
conn.close()
|
||||
return ok, msg
|
||||
|
||||
def _check_roll_monitors():
|
||||
conn = get_db()
|
||||
init_strategy_tables(conn)
|
||||
mode = get_trading_mode(get_setting)
|
||||
try:
|
||||
check_roll_monitors(
|
||||
conn,
|
||||
get_mark_price_fn=lambda sym: _roll_mark_price(sym, {}, mode),
|
||||
fill_roll_leg_fn=_fill_roll_leg_cb,
|
||||
is_trading_session_fn=is_trading_session,
|
||||
get_risk_budget_fn=lambda: get_fixed_amount(get_setting),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
app._check_roll_monitors = _check_roll_monitors
|
||||
|
||||
def _apply_roll_margin_cap(
|
||||
preview: dict,
|
||||
*,
|
||||
@@ -2780,37 +3102,25 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
def api_roll_preview():
|
||||
d = request.get_json(silent=True) or {}
|
||||
conn = get_db()
|
||||
init_strategy_tables(conn)
|
||||
ensure_monitor_order_columns(conn)
|
||||
mon_id = int(d.get("monitor_id") or 0)
|
||||
mon = conn.execute("SELECT * FROM trade_order_monitors WHERE id=? AND status='active'", (mon_id,)).fetchone()
|
||||
conn.close()
|
||||
mon = conn.execute(
|
||||
"SELECT * FROM trade_order_monitors WHERE id=? AND status='active'", (mon_id,),
|
||||
).fetchone()
|
||||
if not mon:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": "无有效持仓监控"}), 400
|
||||
sym = mon["symbol"]
|
||||
spec = get_contract_spec(sym)
|
||||
capital = _capital(get_db())
|
||||
preview, err = preview_roll(
|
||||
direction=mon["direction"],
|
||||
symbol=sym,
|
||||
qty_existing=float(mon["lots"]),
|
||||
entry_existing=float(mon["entry_price"]),
|
||||
initial_take_profit=float(mon["take_profit"] or 0),
|
||||
add_mode=d.get("add_mode") or "market",
|
||||
new_stop_loss=float(d.get("new_stop_loss") or 0),
|
||||
risk_percent=float(d.get("risk_percent") or 2),
|
||||
capital_base=capital,
|
||||
mult=spec["mult"],
|
||||
add_price=float(d.get("add_price") or mon["entry_price"]),
|
||||
fib_upper=d.get("fib_upper"),
|
||||
fib_lower=d.get("fib_lower"),
|
||||
legs_done=int(d.get("legs_done") or 0),
|
||||
)
|
||||
mon_d = dict(mon)
|
||||
err = _roll_eligibility(conn, mon_d)
|
||||
if err:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": err}), 400
|
||||
preview, merr = _apply_roll_margin_cap(
|
||||
preview, conn=conn, mode=get_trading_mode(get_setting), mon=dict(mon), capital=capital,
|
||||
)
|
||||
if merr:
|
||||
return jsonify({"ok": False, "error": merr}), 400
|
||||
mode = get_trading_mode(get_setting)
|
||||
preview, perr = _build_roll_preview(conn, d, mon_d, mode=mode)
|
||||
conn.close()
|
||||
if perr:
|
||||
return jsonify({"ok": False, "error": perr}), 400
|
||||
return jsonify({"ok": True, "preview": preview})
|
||||
|
||||
@app.route("/api/strategy/roll/execute", methods=["POST"])
|
||||
@@ -2819,87 +3129,57 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
d = request.get_json(silent=True) or {}
|
||||
conn = get_db()
|
||||
init_strategy_tables(conn)
|
||||
ensure_monitor_order_columns(conn)
|
||||
mon_id = int(d.get("monitor_id") or 0)
|
||||
mon = conn.execute("SELECT * FROM trade_order_monitors WHERE id=? AND status='active'", (mon_id,)).fetchone()
|
||||
mon = conn.execute(
|
||||
"SELECT * FROM trade_order_monitors WHERE id=? AND status='active'", (mon_id,),
|
||||
).fetchone()
|
||||
if not mon:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": "无有效持仓监控"}), 400
|
||||
if conn.execute("SELECT id FROM trend_pullback_plans WHERE status='active'").fetchone():
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": "趋势回调运行中,不可滚仓"}), 400
|
||||
sym = mon["symbol"]
|
||||
spec = get_contract_spec(sym)
|
||||
capital = _capital(conn)
|
||||
mode = get_trading_mode(get_setting)
|
||||
prev, err = preview_roll(
|
||||
direction=mon["direction"],
|
||||
symbol=sym,
|
||||
qty_existing=float(mon["lots"]),
|
||||
entry_existing=float(mon["entry_price"]),
|
||||
initial_take_profit=float(mon["take_profit"] or 0),
|
||||
add_mode=d.get("add_mode") or "market",
|
||||
new_stop_loss=float(d.get("new_stop_loss") or 0),
|
||||
risk_percent=float(d.get("risk_percent") or 2),
|
||||
capital_base=capital,
|
||||
mult=spec["mult"],
|
||||
add_price=float(d.get("add_price") or mon["entry_price"]),
|
||||
)
|
||||
mon_d = dict(mon)
|
||||
err = _roll_eligibility(conn, mon_d)
|
||||
if err:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": err}), 400
|
||||
prev, merr = _apply_roll_margin_cap(
|
||||
prev, conn=conn, mode=mode, mon=dict(mon), capital=capital,
|
||||
)
|
||||
if merr:
|
||||
mode = get_trading_mode(get_setting)
|
||||
preview, perr = _build_roll_preview(conn, d, mon_d, mode=mode)
|
||||
if perr:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": merr}), 400
|
||||
price = float(prev["add_price"])
|
||||
try:
|
||||
execute_order(
|
||||
conn, mode=mode, offset="open", symbol=sym,
|
||||
direction=mon["direction"], lots=int(prev["add_lots"]), price=price, settings=_settings_dict(),
|
||||
)
|
||||
except ValueError as exc:
|
||||
return jsonify({"ok": False, "error": perr}), 400
|
||||
add_mode = (d.get("add_mode") or ADD_MODE_MARKET).strip().lower()
|
||||
if add_mode in PENDING_MODES:
|
||||
ok, msg = _submit_roll_pending(conn, mon=mon_d, preview=preview, add_mode=add_mode)
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": str(exc)}), 400
|
||||
new_lots = int(mon["lots"]) + int(prev["add_lots"])
|
||||
new_avg = prev["avg_entry_after"]
|
||||
new_sl = prev["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),
|
||||
if not ok:
|
||||
return jsonify({"ok": False, "error": msg}), 400
|
||||
return jsonify({"ok": True, "message": msg, "pending": True})
|
||||
if not is_trading_session():
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": "不在交易时间段"}), 403
|
||||
if not ctp_status(mode).get("connected"):
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": "请先连接 CTP"}), 400
|
||||
ok, msg = _commit_roll_fill(
|
||||
conn, mon=mon_d, preview=preview, add_mode=add_mode, mode=mode,
|
||||
)
|
||||
grp = conn.execute(
|
||||
"SELECT * FROM roll_groups WHERE order_monitor_id=? AND status='active'",
|
||||
(mon_id,),
|
||||
).fetchone()
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
if grp:
|
||||
gid = 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',?,?)""",
|
||||
(mon_id, sym, mon["direction"], mon["take_profit"], mon["stop_loss"], new_sl,
|
||||
float(d.get("risk_percent") or 2), now, now),
|
||||
)
|
||||
gid = cur.lastrowid
|
||||
leg_n = 1
|
||||
conn.execute(
|
||||
"""INSERT INTO roll_legs (roll_group_id, leg_index, add_mode, fill_price, lots, new_stop_loss, status, created_at)
|
||||
VALUES (?,?,?,?,?,?, 'filled', ?)""",
|
||||
(gid, leg_n, d.get("add_mode") or "market", price, int(prev["add_lots"]), new_sl, now),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({"ok": True, "preview": prev})
|
||||
if not ok:
|
||||
return jsonify({"ok": False, "error": msg}), 400
|
||||
return jsonify({"ok": True, "message": msg, "preview": preview})
|
||||
|
||||
@app.route("/api/strategy/roll/cancel/<int:leg_id>", methods=["POST"])
|
||||
@login_required
|
||||
def api_roll_cancel(leg_id: int):
|
||||
conn = get_db()
|
||||
init_strategy_tables(conn)
|
||||
ok, msg = cancel_roll_leg(conn, leg_id)
|
||||
if ok:
|
||||
conn.commit()
|
||||
conn.close()
|
||||
if not ok:
|
||||
return jsonify({"ok": False, "error": msg}), 400
|
||||
return jsonify({"ok": True, "message": msg})
|
||||
|
||||
@app.route("/api/strategy/trend/stop", methods=["POST"])
|
||||
@login_required
|
||||
|
||||
Reference in New Issue
Block a user