Files
crypto_monitor/strategy_register.py
T
2026-05-28 14:43:15 +08:00

327 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""策略交易: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, request, url_for
from jinja2 import ChoiceLoader, FileSystemLoader
from strategy_db import init_strategy_tables
from strategy_roll_lib import preview_roll
def install_strategy_trading(app: Flask, repo_root: str, app_module: Any = None, **build_kw) -> None:
"""在 app.py 末尾调用(login_required 已定义后)。仅注册 POST API;页面由各 app 的 render_main_page 渲染。"""
from strategy_config import build_strategy_config
build_kw.pop("render_trend_page", None)
attach_strategy_templates(app, repo_root)
cfg = build_strategy_config(app_module, **build_kw)
register_strategy_trading(app, cfg)
app.extensions["strategy_roll_cfg"] = cfg
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"]
def _lr(f):
return login_required(f)
@_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_trading_page"))
@_lr
@app.route("/strategy/roll/execute", methods=["POST"])
def strategy_roll_execute():
data = request.form
try:
ok, msg = _roll_execute(cfg, data)
except Exception as e:
fe = cfg.get("friendly_error")
msg = fe(e) if callable(fe) else str(e)
ok = False
flash(msg)
return redirect(url_for("strategy_trading_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:
get_db = cfg["get_db"]
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]:
get_db = cfg["get_db"]
conn = None
try:
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, "加仓张数低于交易所最小精度"
lev_fn = cfg.get("default_leverage")
if not callable(lev_fn):
lev_fn = lambda _s: 5
leverage = int(data.get("leverage") or 0) or int(lev_fn(symbol))
conn = get_db()
init_strategy_tables(conn)
mon = _get_active_monitor(conn, cfg, symbol, direction)
if not mon:
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"])
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()
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()
return True, f"滚仓第 {legs_done + 1} 腿已市价成交,交易所止损已更新,止盈仍为首仓 {tp0}"
except Exception as e:
fe = cfg.get("friendly_error")
return False, fe(e) if callable(fe) else str(e)
finally:
if conn is not None:
try:
conn.close()
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",
(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