refactor: 移除 gate_bot,统一为三所架构并更新文档

删除 crypto_monitor_gate_bot 目录,中控与子代理改为 binance/okx/gate 三账户;
文档与 UI 文案「四所」改为「三所」;新增清库前一次性配置备份脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-04 22:00:08 +08:00
parent be51eee73f
commit 9f67de3677
138 changed files with 26395 additions and 40057 deletions
+230 -230
View File
@@ -1,230 +1,230 @@
"""各交易所 app 模块 → strategy_register 配置(统一工厂)。"""
from __future__ import annotations
import sys
from typing import Any
def resolve_trading_app_module(app_module: Any = None) -> Any:
"""
须在 login_required 定义之后调用。
PM2 / python app.py 时 __name__ 为 __main__,请传入 sys.modules[__name__]。
"""
if app_module is None:
main = sys.modules.get("__main__")
if main is not None and hasattr(main, "login_required"):
m = main
else:
import inspect
m = None
for fr in inspect.stack():
g = fr.frame.f_globals
if callable(g.get("login_required")) and callable(g.get("get_db")):
m = g
break
if m is None:
raise RuntimeError(
"策略交易注册失败:请使用 install_strategy_trading(app, repo_root, app_module=sys.modules[__name__])"
)
else:
m = app_module
if not hasattr(m, "login_required"):
raise RuntimeError(
"策略交易注册须在 login_required 定义之后执行(将 install_strategy_trading 放在 app.py 末尾)"
)
return m
def build_strategy_config(
app_module: Any = None, *, trend_enabled: bool = False, trend_disabled_note: str = ""
) -> dict:
m = resolve_trading_app_module(app_module)
def get_trading_capital_usdt(conn):
if hasattr(m, "get_exchange_capitals"):
_, tc = m.get_exchange_capitals(force=True)
if tc is not None:
return float(tc)
if hasattr(m, "get_available_trading_usdt"):
snap = m.get_available_trading_usdt()
if snap is not None:
return float(snap)
day = m.get_trading_day(m.app_now())
row = m.ensure_session(conn, day)
return float(row["current_capital"])
def get_position(ex_sym, direction):
qty = m.get_live_position_contracts(ex_sym, direction)
entry = None
try:
rows = m.exchange.fetch_positions([ex_sym])
for p in rows or []:
matcher = getattr(m, "_row_matches_monitor_direction", None)
if matcher and not matcher(direction, p):
continue
contracts = getattr(m, "_position_row_effective_contracts", lambda x: abs(float(x.get("contracts") or 0)))(p)
if contracts <= 0:
continue
coerce = getattr(m, "_coerce_float", None)
if coerce:
entry = coerce(
p.get("entryPrice"),
p.get("average"),
(p.get("info") or {}).get("entryPrice"),
)
if entry:
break
except Exception:
pass
return {"contracts": float(qty or 0), "entry_price": entry}
def amount_to_precision(ex_sym, amount):
try:
return float(m.exchange.amount_to_precision(ex_sym, float(amount)))
except Exception:
return None
def price_to_precision(ex_sym, price):
try:
return float(m.exchange.price_to_precision(ex_sym, float(price)))
except Exception:
return None
def market_add(ex_sym, direction, amount, leverage):
return m.place_exchange_order(ex_sym, direction, amount, leverage, stop_loss=None, take_profit=None)
def limit_add(ex_sym, direction, amount, price, leverage):
m.exchange.set_leverage(int(leverage), ex_sym)
side = "buy" if direction == "long" else "sell"
if hasattr(m, "build_okx_order_params"):
params = m.build_okx_order_params(direction, reduce_only=False)
elif hasattr(m, "build_binance_order_params"):
params = m.build_binance_order_params(direction, reduce_only=False)
elif hasattr(m, "build_gate_order_params"):
params = m.build_gate_order_params(direction, reduce_only=False)
else:
params = {}
return m.exchange.create_order(
ex_sym, "limit", side, float(amount), float(price), params if params is not None else {}
)
def replace_tpsl(ex_sym, direction, sl, tp, order_row):
row = order_row or {"symbol": ex_sym, "exchange_symbol": ex_sym, "direction": direction}
m.replace_active_monitor_tpsl_on_exchange(row, sl, tp)
def count_trends(conn):
try:
return int(
conn.execute(
"SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'"
).fetchone()[0]
)
except Exception:
return 0
def friendly_error(err):
fn = getattr(m, "friendly_exchange_error", None) or getattr(
m, "friendly_okx_error", None
)
if not callable(fn):
return str(err)
try:
snap = m.get_available_trading_usdt()
except Exception:
snap = None
try:
return fn(err, available_usdt=snap)
except TypeError:
return fn(err)
def limit_order_status(ex_sym, order_id):
fn = getattr(m, "fib_limit_order_status", None)
if callable(fn):
return fn(ex_sym, order_id)
return "unknown"
def cancel_limit_order(ex_sym, order_id):
fn = getattr(m, "cancel_fib_limit_order", None)
if callable(fn):
try:
return fn(ex_sym, order_id)
except Exception:
pass
if not order_id:
return False
try:
m.exchange.cancel_order(str(order_id), ex_sym)
return True
except Exception:
return False
def get_mark_price(symbol):
fn = getattr(m, "get_symbol_mark_price", None) or getattr(m, "get_price", None)
if not callable(fn):
return None
try:
return fn(symbol)
except Exception:
return None
def wechat_account_label():
fn = getattr(m, "_wechat_account_label", None)
if callable(fn):
try:
return fn()
except Exception:
pass
return getattr(m, "EXCHANGE_DISPLAY_NAME", "") or ""
def wechat_direction_text(direction):
fn = getattr(m, "_wechat_direction_text", None)
if callable(fn):
try:
return fn(direction)
except Exception:
pass
d = (direction or "long").strip().lower()
return "做多" if d == "long" else "做空"
def send_wechat(content):
fn = getattr(m, "send_wechat_msg", None)
if callable(fn):
fn(content)
note = trend_disabled_note or (
"趋势回调(自动补仓)请在 Gate 趋势机器人实例使用:/strategy/trend"
)
return {
"app_module": m,
"exchange_display": getattr(m, "EXCHANGE_DISPLAY_NAME", ""),
"trend_enabled": trend_enabled,
"trend_disabled_note": note,
"login_required": m.login_required,
"get_db": m.get_db,
"normalize_symbol_input": m.normalize_symbol_input,
"normalize_exchange_symbol": m.normalize_exchange_symbol,
"get_price": m.get_price,
"get_trading_capital_usdt": get_trading_capital_usdt,
"get_position": get_position,
"amount_to_precision": amount_to_precision,
"price_to_precision": price_to_precision,
"market_add": market_add,
"limit_add": limit_add,
"replace_tpsl": replace_tpsl,
"ensure_live_ready": m.ensure_exchange_live_ready,
"default_risk_percent": float(getattr(m, "RISK_PERCENT", 2)),
"default_leverage": m.infer_leverage,
"friendly_error": friendly_error,
"app_now_str": m.app_now_str,
"resolve_fill_price": m.resolve_order_entry_price,
"price_fmt": m.format_price_for_symbol,
"count_active_trend_plans": count_trends if trend_enabled else count_trends,
"limit_order_status": limit_order_status,
"cancel_limit_order": cancel_limit_order,
"get_mark_price": get_mark_price,
"send_wechat": send_wechat,
"format_price": getattr(m, "format_price_for_symbol", None),
"wechat_account_label": wechat_account_label,
"wechat_direction_text": wechat_direction_text,
}
"""各交易所 app 模块 → strategy_register 配置(统一工厂)。"""
from __future__ import annotations
import sys
from typing import Any
def resolve_trading_app_module(app_module: Any = None) -> Any:
"""
须在 login_required 定义之后调用。
PM2 / python app.py 时 __name__ 为 __main__,请传入 sys.modules[__name__]。
"""
if app_module is None:
main = sys.modules.get("__main__")
if main is not None and hasattr(main, "login_required"):
m = main
else:
import inspect
m = None
for fr in inspect.stack():
g = fr.frame.f_globals
if callable(g.get("login_required")) and callable(g.get("get_db")):
m = g
break
if m is None:
raise RuntimeError(
"策略交易注册失败:请使用 install_strategy_trading(app, repo_root, app_module=sys.modules[__name__])"
)
else:
m = app_module
if not hasattr(m, "login_required"):
raise RuntimeError(
"策略交易注册须在 login_required 定义之后执行(将 install_strategy_trading 放在 app.py 末尾)"
)
return m
def build_strategy_config(
app_module: Any = None, *, trend_enabled: bool = False, trend_disabled_note: str = ""
) -> dict:
m = resolve_trading_app_module(app_module)
def get_trading_capital_usdt(conn):
if hasattr(m, "get_exchange_capitals"):
_, tc = m.get_exchange_capitals(force=True)
if tc is not None:
return float(tc)
if hasattr(m, "get_available_trading_usdt"):
snap = m.get_available_trading_usdt()
if snap is not None:
return float(snap)
day = m.get_trading_day(m.app_now())
row = m.ensure_session(conn, day)
return float(row["current_capital"])
def get_position(ex_sym, direction):
qty = m.get_live_position_contracts(ex_sym, direction)
entry = None
try:
rows = m.exchange.fetch_positions([ex_sym])
for p in rows or []:
matcher = getattr(m, "_row_matches_monitor_direction", None)
if matcher and not matcher(direction, p):
continue
contracts = getattr(m, "_position_row_effective_contracts", lambda x: abs(float(x.get("contracts") or 0)))(p)
if contracts <= 0:
continue
coerce = getattr(m, "_coerce_float", None)
if coerce:
entry = coerce(
p.get("entryPrice"),
p.get("average"),
(p.get("info") or {}).get("entryPrice"),
)
if entry:
break
except Exception:
pass
return {"contracts": float(qty or 0), "entry_price": entry}
def amount_to_precision(ex_sym, amount):
try:
return float(m.exchange.amount_to_precision(ex_sym, float(amount)))
except Exception:
return None
def price_to_precision(ex_sym, price):
try:
return float(m.exchange.price_to_precision(ex_sym, float(price)))
except Exception:
return None
def market_add(ex_sym, direction, amount, leverage):
return m.place_exchange_order(ex_sym, direction, amount, leverage, stop_loss=None, take_profit=None)
def limit_add(ex_sym, direction, amount, price, leverage):
m.exchange.set_leverage(int(leverage), ex_sym)
side = "buy" if direction == "long" else "sell"
if hasattr(m, "build_okx_order_params"):
params = m.build_okx_order_params(direction, reduce_only=False)
elif hasattr(m, "build_binance_order_params"):
params = m.build_binance_order_params(direction, reduce_only=False)
elif hasattr(m, "build_gate_order_params"):
params = m.build_gate_order_params(direction, reduce_only=False)
else:
params = {}
return m.exchange.create_order(
ex_sym, "limit", side, float(amount), float(price), params if params is not None else {}
)
def replace_tpsl(ex_sym, direction, sl, tp, order_row):
row = order_row or {"symbol": ex_sym, "exchange_symbol": ex_sym, "direction": direction}
m.replace_active_monitor_tpsl_on_exchange(row, sl, tp)
def count_trends(conn):
try:
return int(
conn.execute(
"SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'"
).fetchone()[0]
)
except Exception:
return 0
def friendly_error(err):
fn = getattr(m, "friendly_exchange_error", None) or getattr(
m, "friendly_okx_error", None
)
if not callable(fn):
return str(err)
try:
snap = m.get_available_trading_usdt()
except Exception:
snap = None
try:
return fn(err, available_usdt=snap)
except TypeError:
return fn(err)
def limit_order_status(ex_sym, order_id):
fn = getattr(m, "fib_limit_order_status", None)
if callable(fn):
return fn(ex_sym, order_id)
return "unknown"
def cancel_limit_order(ex_sym, order_id):
fn = getattr(m, "cancel_fib_limit_order", None)
if callable(fn):
try:
return fn(ex_sym, order_id)
except Exception:
pass
if not order_id:
return False
try:
m.exchange.cancel_order(str(order_id), ex_sym)
return True
except Exception:
return False
def get_mark_price(symbol):
fn = getattr(m, "get_symbol_mark_price", None) or getattr(m, "get_price", None)
if not callable(fn):
return None
try:
return fn(symbol)
except Exception:
return None
def wechat_account_label():
fn = getattr(m, "_wechat_account_label", None)
if callable(fn):
try:
return fn()
except Exception:
pass
return getattr(m, "EXCHANGE_DISPLAY_NAME", "") or ""
def wechat_direction_text(direction):
fn = getattr(m, "_wechat_direction_text", None)
if callable(fn):
try:
return fn(direction)
except Exception:
pass
d = (direction or "long").strip().lower()
return "做多" if d == "long" else "做空"
def send_wechat(content):
fn = getattr(m, "send_wechat_msg", None)
if callable(fn):
fn(content)
note = trend_disabled_note or (
"趋势回调(自动补仓)请在 Gate机器人实例使用:/strategy/trend"
)
return {
"app_module": m,
"exchange_display": getattr(m, "EXCHANGE_DISPLAY_NAME", ""),
"trend_enabled": trend_enabled,
"trend_disabled_note": note,
"login_required": m.login_required,
"get_db": m.get_db,
"normalize_symbol_input": m.normalize_symbol_input,
"normalize_exchange_symbol": m.normalize_exchange_symbol,
"get_price": m.get_price,
"get_trading_capital_usdt": get_trading_capital_usdt,
"get_position": get_position,
"amount_to_precision": amount_to_precision,
"price_to_precision": price_to_precision,
"market_add": market_add,
"limit_add": limit_add,
"replace_tpsl": replace_tpsl,
"ensure_live_ready": m.ensure_exchange_live_ready,
"default_risk_percent": float(getattr(m, "RISK_PERCENT", 2)),
"default_leverage": m.infer_leverage,
"friendly_error": friendly_error,
"app_now_str": m.app_now_str,
"resolve_fill_price": m.resolve_order_entry_price,
"price_fmt": m.format_price_for_symbol,
"count_active_trend_plans": count_trends if trend_enabled else count_trends,
"limit_order_status": limit_order_status,
"cancel_limit_order": cancel_limit_order,
"get_mark_price": get_mark_price,
"send_wechat": send_wechat,
"format_price": getattr(m, "format_price_for_symbol", None),
"wechat_account_label": wechat_account_label,
"wechat_direction_text": wechat_direction_text,
}
+1 -1
View File
@@ -2,7 +2,7 @@
Gate.io USDT 永续 — 策略交易交易所侧能力。
实现方式:各 Gate 实例 app 通过 strategy_config.build_strategy_config(app_module) 注入
ccxt 下单、精度、换 TP/SL;本文件为文档与类型锚点,避免在四个 app 重复实现滚仓公式。
ccxt 下单、精度、换 TP/SL;本文件为文档与类型锚点,避免在 app 重复实现滚仓公式。
"""
from lib.strategy.strategy_exchange_base import StrategyExchangeAdapter
+1 -1
View File
@@ -1,4 +1,4 @@
"""策略交易记录页:已结束趋势 / 顺势加仓快照(所统一)。"""
"""策略交易记录页:已结束趋势 / 顺势加仓快照(所统一)。"""
from __future__ import annotations
import json
+1 -1
View File
@@ -1,4 +1,4 @@
"""策略结束快照:趋势回调 / 顺势加仓(所共用)。"""
"""策略结束快照:趋势回调 / 顺势加仓(所共用)。"""
from __future__ import annotations
import json
+4 -4
View File
@@ -230,7 +230,7 @@ def compute_trend_plan_core(
def calc_planned_reward_risk_ratio(
direction: str, entry_price: float, stop_loss: float, take_profit: float
) -> Optional[float]:
"""盈亏比(reward/risk),与所 calc_rr_ratio 口径一致。"""
"""盈亏比(reward/risk),与所 calc_rr_ratio 口径一致。"""
try:
entry = float(entry_price)
sl = float(stop_loss)
@@ -375,7 +375,7 @@ def trend_leg_grid_price(plan: dict, leg_idx: int) -> Optional[float]:
def trend_leg_display_price(plan: dict, leg_idx: int) -> Optional[float]:
"""
所统一:单档展示价 = leg_fill_prices_json 实际记录,否则计划网格(首仓用均价/参考价)。
所统一:单档展示价 = leg_fill_prices_json 实际记录,否则计划网格(首仓用均价/参考价)。
禁止为凑均价反推虚构成交价。
"""
p = plan or {}
@@ -398,7 +398,7 @@ def trend_leg_display_price(plan: dict, leg_idx: int) -> Optional[float]:
def reconcile_trend_leg_fill_prices(plan: dict) -> list[float]:
"""首仓(0)+已补仓(1..legs_done) 展示价列表(所共用 trend_leg_display_price)。"""
"""首仓(0)+已补仓(1..legs_done) 展示价列表(所共用 trend_leg_display_price)。"""
p = plan or {}
if int(p.get("first_order_done") or 0) == 0:
return []
@@ -563,7 +563,7 @@ def build_trend_preview_level_rows(preview: dict) -> tuple[dict, list[dict]]:
def enrich_trend_dca_levels_with_tp(plan: dict, levels: list[dict]) -> list[dict]:
"""
所统一补仓表 enrich(实例策略页 + 中控 monitor 共用)。
所统一补仓表 enrich(实例策略页 + 中控 monitor 共用)。
触发价:实际成交价或计划网格;末档加仓后均价用持仓均价;禁止反推虚构成交价。
"""
if not levels:
+4 -4
View File
@@ -1,4 +1,4 @@
"""趋势回调:路由、轮询、页面数据(所共用,依赖各 app 模块交易所能力)。"""
"""趋势回调:路由、轮询、页面数据(所共用,依赖各 app 模块交易所能力)。"""
from __future__ import annotations
import inspect
@@ -138,8 +138,8 @@ def summarize_trend_dca_probe(cfg: dict, row) -> dict:
out["block_reason"] = "交易所无持仓"
else:
out["block_reason"] = (
"标记价已触达,轮询应自动下单;若仍未补请确认 PM2 进程 crypto_gate_bot "
"非 manual-agent-gate-bot)在运行,并查看 pm2 logs crypto_gate_bot"
"标记价已触达,轮询应自动下单;若仍未补请确认 PM2 进程 crypto_gate "
"或对应所 Flask 进程)在运行,并查看 pm2 logs"
)
elif not reached:
out["block_reason"] = f"标记价 {pf} 未触达下一档 {level}"
@@ -520,7 +520,7 @@ def _patch_hub_trend_views(app: Flask) -> None:
def patch_trend_hub_enrich(app: Flask, cfg: dict) -> None:
"""hub_bridge install 之后调用:所 /api/hub/monitor 趋势字段与策略页一致。"""
"""hub_bridge install 之后调用:所 /api/hub/monitor 趋势字段与策略页一致。"""
_patch_hub_monitor_enrich(app, cfg)
+2 -3
View File
@@ -77,9 +77,8 @@ def fetch_roll_page_data(
DEFAULT_TREND_DISABLED_NOTE = (
"趋势回调(预览、自动补仓、程序止盈)仅在 Gate 趋势机器人实例 "
"crypto_monitor_gate_bot,常见端口 5002)中启用"
"币安 / Gate 主站 / OKX 可使用本页「顺势加仓」;完整趋势回调请打开该实例。"
"趋势回调(预览、自动补仓、程序止盈)须在本实例 .env 设置 "
"`LIVE_TRADING_ENABLED=true` 并重启对应 PM2 进程(如 crypto_gate / crypto_okx / crypto_binance"
)
+1 -1
View File
@@ -1,4 +1,4 @@
"""策略计划(趋势回调 / 滚仓)开始与结束 — 企业微信推送(所共用)。"""
"""策略计划(趋势回调 / 滚仓)开始与结束 — 企业微信推送(所共用)。"""
from __future__ import annotations
from typing import Any, Optional
@@ -14,7 +14,7 @@
<div class="box">
<h1>趋势回调</h1>
<p>{{ trend_note }}</p>
<p style="color:#8892b0;font-size:.9rem">趋势回调含自动补仓档位,仅在 Gate 趋势机器人(crypto_monitor_gate_bot)实例中运行</p>
<p style="color:#8892b0;font-size:.9rem">趋势回调含自动补仓档位,在三所实例(Binance / Gate / OKX)中均可启用,须配置 LIVE_TRADING_ENABLED=true</p>
</div>
</body>
</html>
@@ -5,8 +5,8 @@
<summary class="tip-collapse-summary">趋势回调说明(本实例未启用)</summary>
<div class="tip-collapse-body rule-tip">
{{ trend_disabled_note }}<br><br>
趋势回调含自动补仓档位与预览执行,<strong>Gate 趋势机器人</strong><code>crypto_monitor_gate_bot</code>)实例中运行。
请访问实例同一菜单「策略交易 → 趋势回调」,或常用地址 <code>:5002/strategy/trend</code>
趋势回调含自动补仓档位与预览执行,在 <strong>Binance / Gate / OKX</strong> 各实例的「策略交易 → 趋势回调」中运行。
请访问对应实例同一菜单,或常用地址如 Gate <code>:5000/strategy/trend</code>
</div>
</details>
<p style="margin-top:12px;font-size:.85rem">
@@ -17,7 +17,7 @@
<strong>计划 #{{ p.plan_id }}</strong> 标记价 {{ p.mark_price }} 已触达补仓触发价 {{ p.next_trigger }},但未自动补仓:
{{ p.block_reason }}。
{% if not live_trading_enabled %}
请在 <code>crypto_monitor_gate_bot/.env</code> 设置 <code>LIVE_TRADING_ENABLED=true</code> 后重启 PM2 进程 <strong>crypto_gate_bot</strong>(不是 manual-agent-gate-bot)。
请在当前实例 <code>.env</code> 设置 <code>LIVE_TRADING_ENABLED=true</code> 后重启对应 PM2 进程(如 <strong>crypto_gate</strong><strong>crypto_okx</strong><strong>crypto_binance</strong>)。
{% endif %}
</div>
{% endif %}