增加策略交易
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
"""策略交易:Flask 路由注册(顺势加仓 + 趋势回调页)。逻辑在 strategy_*_lib。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from functools import wraps
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
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
|
||||
|
||||
|
||||
def attach_strategy_templates(app: Flask, repo_root: str) -> None:
|
||||
strat_dir = os.path.join(repo_root, "strategy_templates")
|
||||
if not os.path.isdir(strat_dir):
|
||||
return
|
||||
existing = app.jinja_loader
|
||||
loaders = [FileSystemLoader(strat_dir)]
|
||||
if existing is not None:
|
||||
if isinstance(existing, ChoiceLoader):
|
||||
loaders = list(existing.loaders) + loaders
|
||||
else:
|
||||
loaders.insert(0, existing)
|
||||
app.jinja_loader = ChoiceLoader(loaders)
|
||||
|
||||
|
||||
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"]
|
||||
trend_enabled = bool(cfg.get("trend_enabled"))
|
||||
render_trend_page = cfg.get("render_trend_page")
|
||||
|
||||
def _lr(f):
|
||||
return login_required(f)
|
||||
|
||||
if trend_enabled and callable(render_trend_page):
|
||||
app.add_url_rule(
|
||||
"/strategy/trend",
|
||||
endpoint="strategy_trend_page",
|
||||
view_func=_lr(render_trend_page),
|
||||
)
|
||||
else:
|
||||
|
||||
@_lr
|
||||
@app.route("/strategy/trend")
|
||||
def strategy_trend_disabled_page():
|
||||
return render_template(
|
||||
"strategy_trend_disabled.html",
|
||||
exchange_display=cfg.get("exchange_display", ""),
|
||||
trend_note=cfg.get(
|
||||
"trend_disabled_note",
|
||||
"趋势回调(自动补仓)当前仅在 Gate 趋势机器人实例中启用。",
|
||||
),
|
||||
)
|
||||
|
||||
@_lr
|
||||
@app.route("/strategy/roll")
|
||||
def strategy_roll_page():
|
||||
conn = get_db()
|
||||
init_strategy_tables(conn)
|
||||
monitors = []
|
||||
for row in conn.execute(
|
||||
"SELECT * FROM order_monitors WHERE status='active' ORDER BY id DESC"
|
||||
).fetchall():
|
||||
monitors.append(_row_to_dict(row))
|
||||
roll_groups = []
|
||||
for row in conn.execute(
|
||||
"SELECT * FROM roll_groups WHERE status='active' ORDER BY id DESC"
|
||||
).fetchall():
|
||||
roll_groups.append(_row_to_dict(row))
|
||||
legs = []
|
||||
for row in conn.execute(
|
||||
"SELECT * FROM roll_legs ORDER BY id DESC LIMIT 50"
|
||||
).fetchall():
|
||||
legs.append(_row_to_dict(row))
|
||||
trend_n = _count_active_trends(conn, cfg)
|
||||
conn.close()
|
||||
return render_template(
|
||||
"strategy_roll.html",
|
||||
page="strategy_roll",
|
||||
exchange_display=cfg.get("exchange_display", ""),
|
||||
monitors=monitors,
|
||||
roll_groups=roll_groups,
|
||||
roll_legs=legs,
|
||||
trend_active=trend_n,
|
||||
default_risk_percent=cfg.get("default_risk_percent", 2),
|
||||
price_fmt=cfg.get("price_fmt"),
|
||||
)
|
||||
|
||||
@_lr
|
||||
@app.route("/strategy/roll/preview", methods=["POST"])
|
||||
def strategy_roll_preview():
|
||||
data = request.get_json(silent=True) or request.form
|
||||
err = _roll_preview_response(cfg, data, json_mode=request.is_json)
|
||||
if request.is_json:
|
||||
return jsonify(err)
|
||||
if err.get("ok"):
|
||||
flash(
|
||||
f"预览:加仓约 {err['preview'].get('add_amount_display', '-')} 张,"
|
||||
f"合并均价 {err['preview'].get('avg_entry_after', '-')},"
|
||||
f"触及新止损亏损约 {err['preview'].get('loss_at_sl_usdt', '-')}U"
|
||||
)
|
||||
else:
|
||||
flash(err.get("msg") or "预览失败")
|
||||
return redirect(url_for("strategy_roll_page"))
|
||||
|
||||
@_lr
|
||||
@app.route("/strategy/roll/execute", methods=["POST"])
|
||||
def strategy_roll_execute():
|
||||
data = request.form
|
||||
ok, msg = _roll_execute(cfg, data)
|
||||
flash(msg)
|
||||
return redirect(url_for("strategy_roll_page"))
|
||||
|
||||
# 趋势回调:仍由各市面 app 注册原有路由;导航指向 /strategy/trend
|
||||
|
||||
|
||||
def _row_to_dict(row) -> dict:
|
||||
if row is None:
|
||||
return {}
|
||||
try:
|
||||
return dict(row)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _count_active_trends(conn, cfg: dict) -> int:
|
||||
fn = cfg.get("count_active_trend_plans")
|
||||
if callable(fn):
|
||||
return int(fn(conn) or 0)
|
||||
try:
|
||||
return int(
|
||||
conn.execute(
|
||||
"SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'"
|
||||
).fetchone()[0]
|
||||
)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def _roll_preview_response(cfg: dict, data: dict, json_mode: bool = False) -> dict:
|
||||
symbol = cfg["normalize_symbol_input"](data.get("symbol") or "")
|
||||
if not symbol:
|
||||
return {"ok": False, "msg": "请选择或填写币种"}
|
||||
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": "存在运行中的趋势回调计划,请先结束后再滚仓"}
|
||||
mon = _get_active_monitor(conn, cfg, symbol, direction)
|
||||
if not mon:
|
||||
conn.close()
|
||||
return {"ok": False, "msg": "未找到该币种同向的下单监控持仓,请先在「实盘下单」开仓"}
|
||||
rg, legs_done = _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:
|
||||
new_sl = float(data.get("new_stop_loss") or data.get("sl"))
|
||||
risk_pct = float(data.get("risk_percent") or cfg.get("default_risk_percent", 2))
|
||||
except (TypeError, ValueError):
|
||||
return {"ok": False, "msg": "止损或风险%格式错误"}
|
||||
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
|
||||
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"))
|
||||
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,
|
||||
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,
|
||||
)
|
||||
if err:
|
||||
return {"ok": False, "msg": err}
|
||||
amt_raw = float(preview["add_amount_raw"])
|
||||
amt_p = cfg["amount_to_precision"](ex_sym, amt_raw)
|
||||
preview["add_amount_display"] = amt_p if amt_p is not None else amt_raw
|
||||
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"])
|
||||
return {"ok": True, "preview": preview}
|
||||
|
||||
|
||||
def _roll_execute(cfg: dict, data: dict) -> tuple[bool, str]:
|
||||
ok_live, reason = cfg["ensure_live_ready"]()
|
||||
if not ok_live:
|
||||
return False, reason or "实盘未就绪"
|
||||
prev = _roll_preview_response(cfg, data)
|
||||
if not prev.get("ok"):
|
||||
return False, prev.get("msg") or "预览失败"
|
||||
preview = prev["preview"]
|
||||
symbol = cfg["normalize_symbol_input"](data.get("symbol") or "")
|
||||
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, "加仓张数低于交易所最小精度"
|
||||
leverage = int(data.get("leverage") or 0) or int(cfg.get("default_leverage", lambda s: 5)(symbol))
|
||||
conn = get_db()
|
||||
init_strategy_tables(conn)
|
||||
mon = _get_active_monitor(conn, cfg, symbol, direction)
|
||||
if not mon:
|
||||
conn.close()
|
||||
return False, "监控单已不存在"
|
||||
rg, legs_done = _get_or_create_roll_group_meta(conn, mon)
|
||||
new_sl = float(preview["new_stop_loss"])
|
||||
tp0 = float(preview["initial_take_profit"])
|
||||
try:
|
||||
if add_mode == "market":
|
||||
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 ""
|
||||
else:
|
||||
price = cfg["price_to_precision"](ex_sym, float(preview["add_price"]))
|
||||
order = cfg["limit_add"](ex_sym, direction, amount, price, leverage)
|
||||
oid = str(order.get("id") or "") if isinstance(order, dict) else ""
|
||||
conn.execute(
|
||||
"""INSERT INTO roll_legs (
|
||||
roll_group_id, leg_index, add_mode, fib_upper, fib_lower, limit_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,
|
||||
amount,
|
||||
new_sl,
|
||||
oid,
|
||||
"pending",
|
||||
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"]),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True, f"已挂限价加仓单 #{oid},成交后请在页面点「同步持仓并更新止损」"
|
||||
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,
|
||||
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"),
|
||||
None,
|
||||
fill,
|
||||
amount,
|
||||
new_sl,
|
||||
oid,
|
||||
"filled",
|
||||
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()
|
||||
conn.close()
|
||||
return True, f"滚仓第 {legs_done + 1} 腿已市价成交,交易所止损已更新,止盈仍为首仓 {tp0}"
|
||||
except Exception as e:
|
||||
conn.close()
|
||||
fe = cfg.get("friendly_error")
|
||||
return False, fe(e) if callable(fe) else str(e)
|
||||
|
||||
|
||||
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",
|
||||
(symbol, direction),
|
||||
).fetchone()
|
||||
return _row_to_dict(row) if row else None
|
||||
|
||||
|
||||
def _get_or_create_roll_group_meta(conn, mon: dict) -> tuple[dict, int]:
|
||||
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)
|
||||
now = mon.get("created_at") or ""
|
||||
cur = conn.execute(
|
||||
"""INSERT INTO roll_groups (
|
||||
order_monitor_id, symbol, exchange_symbol, direction,
|
||||
initial_take_profit, initial_stop_loss, current_stop_loss,
|
||||
risk_percent, leg_count, status, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
mon["id"],
|
||||
mon["symbol"],
|
||||
mon.get("exchange_symbol"),
|
||||
mon["direction"],
|
||||
mon.get("take_profit"),
|
||||
mon.get("stop_loss"),
|
||||
mon.get("stop_loss"),
|
||||
mon.get("risk_percent") or 2,
|
||||
0,
|
||||
"active",
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
gid = int(cur.lastrowid)
|
||||
return {"id": gid, "leg_count": 0, "initial_take_profit": mon.get("take_profit")}, 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user