顺势加仓 v2:程序监控滚仓、文档页与平仓同步
重写滚仓计仓与四种加仓方式(市价/斐波/突破),程序盯 mark 触价成交;风险读监控单;pending 可删不可改;手动平仓同步结束滚仓。新增 /strategy/roll/docs 说明页与顺势加仓滚仓说明.md。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+334
-160
@@ -1,15 +1,22 @@
|
||||
"""策略交易:Flask 路由注册(顺势加仓 + 趋势回调页)。逻辑在 strategy_*_lib。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import html as html_module
|
||||
import os
|
||||
from functools import wraps
|
||||
from typing import Any, Callable, Optional
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
|
||||
from flask import Flask, flash, jsonify, redirect, request, url_for
|
||||
from flask import Flask, flash, jsonify, redirect, render_template, request, url_for
|
||||
from jinja2 import ChoiceLoader, FileSystemLoader
|
||||
|
||||
from strategy_db import init_strategy_tables
|
||||
from strategy_roll_lib import preview_roll, roll_stop_after_fill
|
||||
from strategy_roll_lib import BREAKOUT_MODE, FIB_MODES, MARKET_MODE, preview_roll
|
||||
from strategy_roll_monitor_lib import (
|
||||
cancel_roll_pending_leg,
|
||||
count_filled_roll_legs,
|
||||
count_pending_roll_legs,
|
||||
sync_roll_after_external_close,
|
||||
)
|
||||
|
||||
|
||||
def _dedupe_strategy_snapshots_on_startup(cfg: dict[str, Any]) -> None:
|
||||
@@ -68,7 +75,6 @@ def register_strategy_trading(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
"""cfg 由各市面 app 注入回调(仅 API / DB 差异)。"""
|
||||
|
||||
login_required = cfg["login_required"]
|
||||
get_db = cfg["get_db"]
|
||||
|
||||
def _lr(f):
|
||||
return login_required(f)
|
||||
@@ -81,10 +87,11 @@ def register_strategy_trading(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
if request.is_json:
|
||||
return jsonify(err)
|
||||
if err.get("ok"):
|
||||
p = err["preview"]
|
||||
flash(
|
||||
f"预览:加仓约 {err['preview'].get('add_amount_display', '-')} 张,"
|
||||
f"合并均价 {err['preview'].get('avg_entry_after', '-')},"
|
||||
f"触及新止损亏损约 {err['preview'].get('loss_at_sl_usdt', '-')}U"
|
||||
f"预览:约 {p.get('add_amount_display', '-')} 张,"
|
||||
f"合并均价 {p.get('avg_entry_after', '-')},"
|
||||
f"打到止损约 {p.get('loss_at_sl_usdt', '-')}U"
|
||||
)
|
||||
else:
|
||||
flash(err.get("msg") or "预览失败")
|
||||
@@ -103,7 +110,109 @@ def register_strategy_trading(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
flash(msg)
|
||||
return redirect(url_for("strategy_trading_page"))
|
||||
|
||||
# 趋势回调:仍由各市面 app 注册原有路由;导航指向 /strategy/trend
|
||||
@_lr
|
||||
@app.route("/strategy/roll/cancel/<int:leg_id>", methods=["POST"])
|
||||
def strategy_roll_cancel_leg(leg_id: int):
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_strategy_tables(conn)
|
||||
ok, msg = cancel_roll_pending_leg(cfg, conn, leg_id)
|
||||
finally:
|
||||
conn.close()
|
||||
if request.is_json:
|
||||
return jsonify({"ok": ok, "msg": msg})
|
||||
flash(msg)
|
||||
return redirect(url_for("strategy_trading_page"))
|
||||
|
||||
@_lr
|
||||
@app.route("/strategy/roll/docs")
|
||||
def strategy_roll_docs():
|
||||
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "顺势加仓滚仓说明.md")
|
||||
if not os.path.isfile(path):
|
||||
flash("滚仓说明文档不存在")
|
||||
return redirect(url_for("strategy_trading_page"))
|
||||
with open(path, encoding="utf-8") as f:
|
||||
raw = f.read()
|
||||
return render_template(
|
||||
"strategy_roll_docs.html",
|
||||
doc_html=_roll_doc_markdown_to_html(raw),
|
||||
exchange_display=cfg.get("exchange_display") or "",
|
||||
)
|
||||
|
||||
|
||||
def _roll_doc_markdown_to_html(text: str) -> str:
|
||||
"""轻量 Markdown → HTML(仅供滚仓说明页)。"""
|
||||
lines = text.splitlines()
|
||||
out: list[str] = []
|
||||
i = 0
|
||||
in_code = False
|
||||
code_buf: list[str] = []
|
||||
|
||||
def flush_code() -> None:
|
||||
nonlocal code_buf
|
||||
if code_buf:
|
||||
out.append(
|
||||
"<pre><code>"
|
||||
+ html_module.escape("\n".join(code_buf))
|
||||
+ "</code></pre>"
|
||||
)
|
||||
code_buf = []
|
||||
|
||||
def inline_md(s: str) -> str:
|
||||
s = html_module.escape(s)
|
||||
s = re.sub(r"`([^`]+)`", r"<code>\1</code>", s)
|
||||
s = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", s)
|
||||
return s
|
||||
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
if line.strip().startswith("```"):
|
||||
if in_code:
|
||||
in_code = False
|
||||
flush_code()
|
||||
else:
|
||||
in_code = True
|
||||
i += 1
|
||||
continue
|
||||
if in_code:
|
||||
code_buf.append(line)
|
||||
i += 1
|
||||
continue
|
||||
if line.startswith("# "):
|
||||
out.append(f"<h1>{inline_md(line[2:].strip())}</h1>")
|
||||
elif line.startswith("## "):
|
||||
out.append(f"<h2>{inline_md(line[3:].strip())}</h2>")
|
||||
elif line.startswith("### "):
|
||||
out.append(f"<h3>{inline_md(line[4:].strip())}</h3>")
|
||||
elif line.strip() == "---":
|
||||
out.append("<hr>")
|
||||
elif line.startswith("|") and "|" in line[1:]:
|
||||
rows: list[str] = []
|
||||
while i < len(lines) and lines[i].startswith("|"):
|
||||
rows.append(lines[i])
|
||||
i += 1
|
||||
if len(rows) >= 2 and re.match(r"^\|[\s\-:|]+\|$", rows[1].strip()):
|
||||
out.append("<table>")
|
||||
hdr = [c.strip() for c in rows[0].strip("|").split("|")]
|
||||
out.append("<tr>" + "".join(f"<th>{inline_md(c)}</th>" for c in hdr) + "</tr>")
|
||||
for row in rows[2:]:
|
||||
cells = [c.strip() for c in row.strip("|").split("|")]
|
||||
out.append("<tr>" + "".join(f"<td>{inline_md(c)}</td>" for c in cells) + "</tr>")
|
||||
out.append("</table>")
|
||||
continue
|
||||
elif re.match(r"^[-*]\s+", line):
|
||||
out.append("<ul>")
|
||||
while i < len(lines) and re.match(r"^[-*]\s+", lines[i]):
|
||||
item = re.sub(r"^[-*]\s+", "", lines[i])
|
||||
out.append(f"<li>{inline_md(item)}</li>")
|
||||
i += 1
|
||||
out.append("</ul>")
|
||||
continue
|
||||
elif line.strip():
|
||||
out.append(f"<p>{inline_md(line.strip())}</p>")
|
||||
i += 1
|
||||
flush_code()
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def _row_to_dict(row) -> dict:
|
||||
@@ -129,8 +238,27 @@ def _count_active_trends(conn, cfg: dict) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _roll_preview_response(cfg: dict, data: dict, json_mode: bool = False) -> dict:
|
||||
"""顺势加仓不占用 MAX_ACTIVE_POSITIONS 新仓名额,故不校验仓位上限冻结。"""
|
||||
def _risk_from_monitor(mon: dict, cfg: dict) -> tuple[Optional[float], Optional[str]]:
|
||||
try:
|
||||
rp = float(mon.get("risk_percent") or cfg.get("default_risk_percent", 2))
|
||||
except (TypeError, ValueError):
|
||||
return None, "监控单风险%无效"
|
||||
if rp <= 0:
|
||||
return None, "监控单风险%须大于0"
|
||||
return rp, None
|
||||
|
||||
|
||||
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 _roll_context(cfg: dict, data: dict) -> tuple[Optional[dict], Optional[str]]:
|
||||
m = cfg.get("app_module")
|
||||
if m is not None:
|
||||
try:
|
||||
@@ -139,99 +267,153 @@ def _roll_preview_response(cfg: dict, data: dict, json_mode: bool = False) -> di
|
||||
mode = getattr(m, "POSITION_SIZING_MODE", None) or "risk"
|
||||
ok_src, src_msg = assert_open_source_allowed(mode, OPEN_SOURCE_ROLL)
|
||||
if not ok_src:
|
||||
return {"ok": False, "msg": src_msg}
|
||||
return None, src_msg
|
||||
except Exception:
|
||||
pass
|
||||
get_db = cfg["get_db"]
|
||||
symbol = cfg["normalize_symbol_input"](data.get("symbol") or "")
|
||||
if not symbol:
|
||||
return {"ok": False, "msg": "请选择或填写币种"}
|
||||
return None, "请选择或填写币种"
|
||||
direction = (data.get("direction") or "long").strip().lower()
|
||||
ex_sym = cfg["normalize_exchange_symbol"](symbol)
|
||||
conn = get_db()
|
||||
init_strategy_tables(conn)
|
||||
if _count_active_trends(conn, cfg) > 0:
|
||||
conn.close()
|
||||
return {"ok": False, "msg": "存在运行中的趋势回调计划,请先结束后再滚仓"}
|
||||
return None, "存在运行中的趋势回调计划,请先结束后再滚仓"
|
||||
mon = _get_active_monitor(conn, cfg, symbol, direction)
|
||||
if not mon:
|
||||
conn.close()
|
||||
return {"ok": False, "msg": "未找到该币种同向的下单监控持仓,请先在「实盘下单」开仓"}
|
||||
rg, legs_done, _is_new = _get_or_create_roll_group_meta(conn, mon)
|
||||
conn.close()
|
||||
pos = cfg["get_position"](ex_sym, direction)
|
||||
qty = float(pos.get("contracts") or 0)
|
||||
if qty <= 0:
|
||||
return {"ok": False, "msg": "交易所无该方向持仓,无法滚仓"}
|
||||
entry = float(pos.get("entry_price") or mon.get("trigger_price") or 0)
|
||||
if entry <= 0:
|
||||
return {"ok": False, "msg": "无法获取持仓均价"}
|
||||
tp0 = float(mon.get("take_profit") or rg.get("initial_take_profit") or 0)
|
||||
add_mode = (data.get("add_mode") or "market").strip().lower()
|
||||
try:
|
||||
risk_pct = float(data.get("risk_percent") or cfg.get("default_risk_percent", 2))
|
||||
except (TypeError, ValueError):
|
||||
return {"ok": False, "msg": "风险%格式错误"}
|
||||
stop_offset_raw = data.get("stop_offset_pct")
|
||||
if stop_offset_raw in (None, ""):
|
||||
stop_offset_raw = data.get("new_stop_loss") or data.get("sl")
|
||||
new_sl_abs = None
|
||||
stop_offset_pct = None
|
||||
if data.get("stop_offset_pct") not in (None, ""):
|
||||
try:
|
||||
stop_offset_pct = float(data.get("stop_offset_pct"))
|
||||
except (TypeError, ValueError):
|
||||
return {"ok": False, "msg": "止损偏移%格式错误"}
|
||||
elif data.get("new_stop_loss") not in (None, "") or data.get("sl") not in (None, ""):
|
||||
try:
|
||||
new_sl_abs = float(data.get("new_stop_loss") or data.get("sl"))
|
||||
except (TypeError, ValueError):
|
||||
return {"ok": False, "msg": "止损格式错误"}
|
||||
elif stop_offset_raw not in (None, ""):
|
||||
try:
|
||||
new_sl_abs = float(stop_offset_raw)
|
||||
except (TypeError, ValueError):
|
||||
return {"ok": False, "msg": "止损格式错误"}
|
||||
return None, "未找到该币种同向的下单监控持仓,请先在「实盘下单」开仓"
|
||||
rg, legs_done, pending, roll_is_new = _get_or_create_roll_group_meta(conn, mon)
|
||||
if pending > 0:
|
||||
conn.close()
|
||||
return None, "已有监控中的滚仓腿,请等待成交/失效或先删除后再提交"
|
||||
conn_cap = get_db()
|
||||
try:
|
||||
capital = float(cfg["get_trading_capital_usdt"](conn_cap))
|
||||
finally:
|
||||
conn_cap.close()
|
||||
live = cfg["get_price"](symbol)
|
||||
fib_u = fib_l = None
|
||||
risk_pct, risk_err = _risk_from_monitor(mon, cfg)
|
||||
if risk_err:
|
||||
conn.close()
|
||||
return None, risk_err
|
||||
pos = cfg["get_position"](ex_sym, direction)
|
||||
qty = float(pos.get("contracts") or 0)
|
||||
if qty <= 0:
|
||||
conn.close()
|
||||
return None, "交易所无该方向持仓,无法滚仓"
|
||||
entry = float(pos.get("entry_price") or mon.get("trigger_price") or 0)
|
||||
if entry <= 0:
|
||||
conn.close()
|
||||
return None, "无法获取持仓均价"
|
||||
mark_fn = cfg.get("get_mark_price") or cfg.get("get_price")
|
||||
mark = mark_fn(symbol) if callable(mark_fn) else cfg["get_price"](symbol)
|
||||
ctx = {
|
||||
"conn": conn,
|
||||
"mon": mon,
|
||||
"rg": rg,
|
||||
"legs_done": legs_done,
|
||||
"symbol": symbol,
|
||||
"direction": direction,
|
||||
"ex_sym": ex_sym,
|
||||
"qty": qty,
|
||||
"entry": entry,
|
||||
"mark": float(mark) if mark else None,
|
||||
"capital": capital,
|
||||
"risk_pct": float(risk_pct),
|
||||
"tp0": float(mon.get("take_profit") or rg.get("initial_take_profit") or 0),
|
||||
"contract_size": _contract_size(cfg, ex_sym),
|
||||
}
|
||||
return ctx, None
|
||||
|
||||
|
||||
def _parse_roll_form(data: dict, ctx: dict) -> tuple[Optional[dict], Optional[str]]:
|
||||
add_mode = (data.get("add_mode") or MARKET_MODE).strip().lower()
|
||||
raw_sl = data.get("new_stop_loss") or data.get("sl")
|
||||
if raw_sl in (None, ""):
|
||||
return None, "请填写新止损价"
|
||||
try:
|
||||
new_sl = float(raw_sl)
|
||||
except (TypeError, ValueError):
|
||||
return None, "止损价格式错误"
|
||||
if new_sl <= 0:
|
||||
return None, "止损价须大于0"
|
||||
fib_u = fib_l = bp = None
|
||||
try:
|
||||
if data.get("fib_upper") not in (None, ""):
|
||||
fib_u = float(data.get("fib_upper"))
|
||||
if data.get("fib_lower") not in (None, ""):
|
||||
fib_l = float(data.get("fib_lower"))
|
||||
if data.get("breakthrough_price") not in (None, ""):
|
||||
bp = float(data.get("breakthrough_price"))
|
||||
except (TypeError, ValueError):
|
||||
return {"ok": False, "msg": "斐波上沿/下沿格式错误"}
|
||||
preview, err = preview_roll(
|
||||
direction=direction,
|
||||
symbol=symbol,
|
||||
qty_existing=qty,
|
||||
entry_existing=entry,
|
||||
initial_take_profit=tp0,
|
||||
add_mode=add_mode,
|
||||
new_stop_loss=new_sl_abs,
|
||||
stop_offset_pct=stop_offset_pct,
|
||||
risk_percent=risk_pct,
|
||||
capital_base_usdt=capital,
|
||||
add_price=float(live) if live else None,
|
||||
fib_upper=fib_u,
|
||||
fib_lower=fib_l,
|
||||
legs_done=legs_done,
|
||||
)
|
||||
return None, "价格参数格式错误"
|
||||
|
||||
add_price = ctx.get("mark")
|
||||
if add_mode == MARKET_MODE:
|
||||
if add_price is None or add_price <= 0:
|
||||
return None, "无法获取市价快照"
|
||||
elif add_mode in FIB_MODES:
|
||||
if fib_u is None or fib_l is None:
|
||||
return None, "斐波须填写上沿 H 与下沿 L"
|
||||
elif add_mode == BREAKOUT_MODE:
|
||||
if bp is None:
|
||||
return None, "突破加仓须填写突破价"
|
||||
add_price = ctx.get("mark")
|
||||
else:
|
||||
return None, "加仓方式无效"
|
||||
|
||||
return {
|
||||
"add_mode": add_mode,
|
||||
"new_stop_loss": new_sl,
|
||||
"fib_upper": fib_u,
|
||||
"fib_lower": fib_l,
|
||||
"breakthrough_price": bp,
|
||||
"add_price": add_price,
|
||||
}, None
|
||||
|
||||
|
||||
def _roll_preview_response(cfg: dict, data: dict, json_mode: bool = False) -> dict:
|
||||
ctx, err = _roll_context(cfg, data)
|
||||
if err:
|
||||
return {"ok": False, "msg": err}
|
||||
parsed, perr = _parse_roll_form(data, ctx)
|
||||
if perr:
|
||||
ctx["conn"].close()
|
||||
return {"ok": False, "msg": perr}
|
||||
conn = ctx["conn"]
|
||||
try:
|
||||
preview, perr2 = preview_roll(
|
||||
direction=ctx["direction"],
|
||||
symbol=ctx["symbol"],
|
||||
qty_existing=ctx["qty"],
|
||||
entry_existing=ctx["entry"],
|
||||
initial_take_profit=ctx["tp0"],
|
||||
add_mode=parsed["add_mode"],
|
||||
new_stop_loss=parsed["new_stop_loss"],
|
||||
risk_percent=ctx["risk_pct"],
|
||||
capital_base_usdt=ctx["capital"],
|
||||
add_price=parsed["add_price"],
|
||||
fib_upper=parsed["fib_upper"],
|
||||
fib_lower=parsed["fib_lower"],
|
||||
breakthrough_price=parsed["breakthrough_price"],
|
||||
legs_done=ctx["legs_done"],
|
||||
contract_size=ctx["contract_size"],
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
if perr2:
|
||||
return {"ok": False, "msg": perr2}
|
||||
amt_raw = float(preview["add_amount_raw"])
|
||||
amt_p = cfg["amount_to_precision"](ex_sym, amt_raw)
|
||||
amt_p = cfg["amount_to_precision"](ctx["ex_sym"], amt_raw)
|
||||
preview["add_amount_display"] = amt_p if amt_p is not None else amt_raw
|
||||
preview["risk_display"] = f"{ctx['risk_pct']:g}%≈{ctx['capital'] * ctx['risk_pct'] / 100:.2f}U"
|
||||
price_fmt = cfg.get("price_fmt")
|
||||
if callable(price_fmt):
|
||||
preview["add_price_display"] = price_fmt(symbol, preview["add_price"])
|
||||
preview["new_sl_display"] = price_fmt(symbol, preview["new_stop_loss"])
|
||||
preview["tp_display"] = price_fmt(symbol, preview["initial_take_profit"])
|
||||
preview["add_price_display"] = price_fmt(ctx["symbol"], preview["add_price"])
|
||||
preview["new_sl_display"] = price_fmt(ctx["symbol"], preview["new_stop_loss"])
|
||||
preview["tp_display"] = price_fmt(ctx["symbol"], preview["initial_take_profit"])
|
||||
return {"ok": True, "preview": preview}
|
||||
|
||||
|
||||
@@ -250,9 +432,8 @@ def _roll_execute(cfg: dict, data: dict) -> tuple[bool, str]:
|
||||
direction = preview["direction"]
|
||||
ex_sym = cfg["normalize_exchange_symbol"](symbol)
|
||||
add_mode = preview["add_mode"]
|
||||
amount = cfg["amount_to_precision"](ex_sym, float(preview["add_amount_raw"]))
|
||||
if amount is None or amount <= 0:
|
||||
return False, "加仓张数低于交易所最小精度"
|
||||
new_sl = float(preview["new_stop_loss"])
|
||||
tp0 = float(preview["initial_take_profit"])
|
||||
lev_fn = cfg.get("default_leverage")
|
||||
if not callable(lev_fn):
|
||||
lev_fn = lambda _s: 5
|
||||
@@ -262,123 +443,86 @@ def _roll_execute(cfg: dict, data: dict) -> tuple[bool, str]:
|
||||
mon = _get_active_monitor(conn, cfg, symbol, direction)
|
||||
if not mon:
|
||||
return False, "监控单已不存在"
|
||||
rg, legs_done, roll_is_new = _get_or_create_roll_group_meta(conn, mon)
|
||||
new_sl = float(preview["new_stop_loss"])
|
||||
stop_offset_pct = preview.get("stop_offset_pct")
|
||||
tp0 = float(preview["initial_take_profit"])
|
||||
qty_before = float(preview.get("qty_existing") or 0)
|
||||
entry_before = float(preview.get("entry_existing") or 0)
|
||||
if add_mode == "market":
|
||||
rg, legs_done, pending, roll_is_new = _get_or_create_roll_group_meta(conn, mon)
|
||||
if pending > 0:
|
||||
return False, "已有监控中的滚仓腿,请先删除或等待结束"
|
||||
if add_mode == MARKET_MODE:
|
||||
amount = cfg["amount_to_precision"](ex_sym, float(preview["add_amount_raw"]))
|
||||
if amount is None or amount <= 0:
|
||||
return False, "加仓张数低于交易所最小精度"
|
||||
order = cfg["market_add"](ex_sym, direction, amount, leverage)
|
||||
fill = float(cfg.get("resolve_fill_price", lambda o, s, p: p)(order, ex_sym, preview["add_price"]) or preview["add_price"])
|
||||
status = "filled"
|
||||
oid = str(order.get("id") or "") if isinstance(order, dict) else ""
|
||||
if stop_offset_pct is not None and qty_before > 0 and entry_before > 0:
|
||||
new_sl = roll_stop_after_fill(
|
||||
direction,
|
||||
qty_before,
|
||||
entry_before,
|
||||
float(amount),
|
||||
fill,
|
||||
stop_offset_pct=float(stop_offset_pct),
|
||||
fill = float(
|
||||
cfg.get("resolve_fill_price", lambda o, s, p: p)(
|
||||
order, ex_sym, preview["add_price"]
|
||||
)
|
||||
px_fn = cfg.get("price_to_precision")
|
||||
if callable(px_fn):
|
||||
new_sl = float(px_fn(ex_sym, new_sl) or new_sl)
|
||||
else:
|
||||
price = cfg["price_to_precision"](ex_sym, float(preview["add_price"]))
|
||||
order = cfg["limit_add"](ex_sym, direction, amount, price, leverage)
|
||||
or preview["add_price"]
|
||||
)
|
||||
oid = str(order.get("id") or "") if isinstance(order, dict) else ""
|
||||
cfg["replace_tpsl"](ex_sym, direction, new_sl, tp0, mon)
|
||||
conn.execute(
|
||||
"""INSERT INTO roll_legs (
|
||||
roll_group_id, leg_index, add_mode, fib_upper, fib_lower, limit_price,
|
||||
amount, new_stop_loss, stop_offset_pct, exchange_order_id, status, created_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
breakthrough_price, fill_price, amount, new_stop_loss, exchange_order_id,
|
||||
status, created_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
rg["id"],
|
||||
legs_done + 1,
|
||||
preview["add_mode_label"],
|
||||
preview.get("fib_upper"),
|
||||
preview.get("fib_lower"),
|
||||
price,
|
||||
None,
|
||||
preview.get("breakthrough_price"),
|
||||
fill,
|
||||
amount,
|
||||
new_sl,
|
||||
stop_offset_pct,
|
||||
oid,
|
||||
"pending",
|
||||
"filled",
|
||||
cfg["app_now_str"](),
|
||||
),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE roll_groups SET leg_count=?, updated_at=? WHERE id=?",
|
||||
(legs_done + 1, cfg["app_now_str"](), rg["id"]),
|
||||
"UPDATE roll_groups SET leg_count=?, current_stop_loss=?, updated_at=? WHERE id=?",
|
||||
(legs_done + 1, new_sl, cfg["app_now_str"](), rg["id"]),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE order_monitors SET stop_loss=? WHERE id=?",
|
||||
(new_sl, mon["id"]),
|
||||
)
|
||||
conn.commit()
|
||||
if roll_is_new:
|
||||
try:
|
||||
from strategy_wechat_notify import notify_roll_group_started
|
||||
|
||||
notify_roll_group_started(
|
||||
cfg,
|
||||
group_id=int(rg["id"]),
|
||||
symbol=symbol,
|
||||
direction=direction,
|
||||
order_monitor_id=int(mon["id"]),
|
||||
initial_take_profit=tp0,
|
||||
initial_stop_loss=float(mon.get("stop_loss") or new_sl),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return True, f"已挂限价加仓单 #{oid},成交后请在页面点「同步持仓并更新止损」"
|
||||
cfg["replace_tpsl"](ex_sym, direction, new_sl, tp0, mon)
|
||||
_maybe_notify_roll_started(cfg, rg, mon, symbol, direction, tp0, new_sl, roll_is_new=roll_is_new)
|
||||
return True, f"市价加仓第 {legs_done + 1} 腿已成交,止损已更新,止盈仍为首仓"
|
||||
# 程序监控:斐波 / 突破
|
||||
limit_px = None
|
||||
if add_mode in FIB_MODES:
|
||||
px_fn = cfg.get("price_to_precision")
|
||||
limit_px = float(preview["add_price"])
|
||||
if callable(px_fn):
|
||||
limit_px = float(px_fn(ex_sym, limit_px) or limit_px)
|
||||
mark_fn = cfg.get("get_mark_price") or cfg.get("get_price")
|
||||
last_mark = mark_fn(symbol) if callable(mark_fn) else preview["add_price"]
|
||||
conn.execute(
|
||||
"""INSERT INTO roll_legs (
|
||||
roll_group_id, leg_index, add_mode, fib_upper, fib_lower, limit_price,
|
||||
fill_price, amount, new_stop_loss, stop_offset_pct, exchange_order_id, status, created_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
breakthrough_price, new_stop_loss, last_mark_price, status, created_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
rg["id"],
|
||||
legs_done + 1,
|
||||
preview["add_mode_label"],
|
||||
preview.get("fib_upper"),
|
||||
preview.get("fib_lower"),
|
||||
None,
|
||||
fill,
|
||||
amount,
|
||||
limit_px,
|
||||
preview.get("breakthrough_price"),
|
||||
new_sl,
|
||||
stop_offset_pct,
|
||||
oid,
|
||||
"filled",
|
||||
last_mark,
|
||||
"pending",
|
||||
cfg["app_now_str"](),
|
||||
),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE roll_groups SET leg_count=?, current_stop_loss=?, updated_at=? WHERE id=?",
|
||||
(legs_done + 1, new_sl, cfg["app_now_str"](), rg["id"]),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE order_monitors SET stop_loss=? WHERE id=?",
|
||||
(new_sl, mon["id"]),
|
||||
)
|
||||
conn.commit()
|
||||
try:
|
||||
from strategy_wechat_notify import (
|
||||
notify_roll_group_started,
|
||||
)
|
||||
|
||||
if roll_is_new:
|
||||
notify_roll_group_started(
|
||||
cfg,
|
||||
group_id=int(rg["id"]),
|
||||
symbol=symbol,
|
||||
direction=direction,
|
||||
order_monitor_id=int(mon["id"]),
|
||||
initial_take_profit=tp0,
|
||||
initial_stop_loss=new_sl,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return True, f"滚仓第 {legs_done + 1} 腿已市价成交,交易所止损已更新,止盈仍为首仓 {tp0}"
|
||||
_maybe_notify_roll_started(cfg, rg, mon, symbol, direction, tp0, new_sl, roll_is_new=roll_is_new)
|
||||
return True, f"已提交{preview['add_mode_label']}监控,触价后将市价加仓并更新止损"
|
||||
except Exception as e:
|
||||
fe = cfg.get("friendly_error")
|
||||
return False, fe(e) if callable(fe) else str(e)
|
||||
@@ -390,6 +534,25 @@ def _roll_execute(cfg: dict, data: dict) -> tuple[bool, str]:
|
||||
pass
|
||||
|
||||
|
||||
def _maybe_notify_roll_started(cfg, rg, mon, symbol, direction, tp0, new_sl, *, roll_is_new: bool) -> None:
|
||||
if not roll_is_new:
|
||||
return
|
||||
try:
|
||||
from strategy_wechat_notify import notify_roll_group_started
|
||||
|
||||
notify_roll_group_started(
|
||||
cfg,
|
||||
group_id=int(rg["id"]),
|
||||
symbol=symbol,
|
||||
direction=direction,
|
||||
order_monitor_id=int(mon["id"]),
|
||||
initial_take_profit=tp0,
|
||||
initial_stop_loss=float(mon.get("stop_loss") or new_sl),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _get_active_monitor(conn, cfg: dict, symbol: str, direction: str) -> Optional[dict]:
|
||||
row = conn.execute(
|
||||
"SELECT * FROM order_monitors WHERE status='active' AND symbol=? AND direction=? ORDER BY id DESC LIMIT 1",
|
||||
@@ -398,14 +561,18 @@ def _get_active_monitor(conn, cfg: dict, symbol: str, direction: str) -> Optiona
|
||||
return _row_to_dict(row) if row else None
|
||||
|
||||
|
||||
def _get_or_create_roll_group_meta(conn, mon: dict) -> tuple[dict, int, bool]:
|
||||
def _get_or_create_roll_group_meta(conn, mon: dict) -> tuple[dict, int, int, bool]:
|
||||
"""返回 (roll_group, filled_legs, pending_legs, is_new_group)。"""
|
||||
row = conn.execute(
|
||||
"SELECT * FROM roll_groups WHERE order_monitor_id=? AND status='active' ORDER BY id DESC LIMIT 1",
|
||||
(mon["id"],),
|
||||
).fetchone()
|
||||
if row:
|
||||
d = _row_to_dict(row)
|
||||
return d, int(d.get("leg_count") or 0), False
|
||||
gid = int(d["id"])
|
||||
filled = count_filled_roll_legs(conn, gid)
|
||||
pending = count_pending_roll_legs(conn, gid)
|
||||
return d, filled, pending, False
|
||||
now = mon.get("created_at") or ""
|
||||
cur = conn.execute(
|
||||
"""INSERT INTO roll_groups (
|
||||
@@ -439,7 +606,14 @@ def _get_or_create_roll_group_meta(conn, mon: dict) -> tuple[dict, int, bool]:
|
||||
"direction": mon.get("direction"),
|
||||
},
|
||||
0,
|
||||
0,
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
def roll_sync_after_external_close(cfg: dict, conn, symbol: str, direction: str) -> dict:
|
||||
"""供 hub / del_order 调用的滚仓同步入口。"""
|
||||
return sync_roll_after_external_close(
|
||||
cfg, conn, symbol, direction, reason="手动平仓,滚仓监控已结束"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user