增加策略交易
This commit is contained in:
@@ -29,6 +29,7 @@ cd crypto_monitor
|
||||
| `crypto_monitor_gate_bot/` | Gate.io 永续(机器人;含趋势回调等) | [部署文档.md](./crypto_monitor_gate_bot/部署文档.md) · [趋势回调策略说明.md](./crypto_monitor_gate_bot/趋势回调策略说明.md) |
|
||||
| `crypto_monitor_okx/` | OKX 永续 | [部署文档.md](./crypto_monitor_okx/部署文档.md) |
|
||||
| `manual_trading_hub/` | 多账户中控(监控 + 紧急全平 + 登录;**不在中控网页下单**) | [README.md](./manual_trading_hub/README.md) · [使用说明.md](./manual_trading_hub/使用说明.md) · [部署文档.md](./manual_trading_hub/部署文档.md) · [常见问题.md](./manual_trading_hub/常见问题.md) |
|
||||
| 根目录 `strategy_*.py` | **策略交易**(趋势回调 + 顺势加仓共用逻辑) | [策略交易说明.md](./策略交易说明.md) |
|
||||
|
||||
前四列为四个 **`crypto_monitor_*`** 交易/监控应用;`manual_trading_hub` 与四者 **进程独立**,无需改四者代码即可并行使用。
|
||||
|
||||
|
||||
@@ -1453,11 +1453,20 @@ def init_db():
|
||||
close_reason TEXT, closed_at TEXT)"""
|
||||
)
|
||||
|
||||
from strategy_db import init_strategy_tables
|
||||
|
||||
init_strategy_tables(conn)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
init_db()
|
||||
|
||||
from strategy_config import build_strategy_config
|
||||
from strategy_register import attach_strategy_templates, register_strategy_trading
|
||||
|
||||
attach_strategy_templates(app, _REPO_ROOT)
|
||||
register_strategy_trading(app, build_strategy_config(sys.modules[__name__]))
|
||||
|
||||
def get_db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
@@ -218,6 +218,8 @@
|
||||
<div class="top-nav">
|
||||
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
|
||||
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">实盘下单</a>
|
||||
<a href="/strategy/trend" class="{% if page == 'strategy_trend' %}active{% endif %}">策略·趋势回调</a>
|
||||
<a href="/strategy/roll" class="{% if page == 'strategy_roll' %}active{% endif %}">策略·顺势加仓</a>
|
||||
<a href="/records" class="{% if page == 'records' %}active{% endif %}">交易记录与复盘</a>
|
||||
<a href="/stats" class="{% if page == 'stats' %}active{% endif %}">统计分析</a>
|
||||
</div>
|
||||
|
||||
@@ -1451,11 +1451,20 @@ def init_db():
|
||||
close_reason TEXT, closed_at TEXT)"""
|
||||
)
|
||||
|
||||
from strategy_db import init_strategy_tables
|
||||
|
||||
init_strategy_tables(conn)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
init_db()
|
||||
|
||||
from strategy_config import build_strategy_config
|
||||
from strategy_register import attach_strategy_templates, register_strategy_trading
|
||||
|
||||
attach_strategy_templates(app, _REPO_ROOT)
|
||||
register_strategy_trading(app, build_strategy_config(sys.modules[__name__]))
|
||||
|
||||
def get_db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
@@ -218,6 +218,8 @@
|
||||
<div class="top-nav">
|
||||
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
|
||||
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">实盘下单</a>
|
||||
<a href="/strategy/trend" class="{% if page == 'strategy_trend' %}active{% endif %}">策略·趋势回调</a>
|
||||
<a href="/strategy/roll" class="{% if page == 'strategy_roll' %}active{% endif %}">策略·顺势加仓</a>
|
||||
<a href="/records" class="{% if page == 'records' %}active{% endif %}">交易记录与复盘</a>
|
||||
<a href="/stats" class="{% if page == 'stats' %}active{% endif %}">统计分析</a>
|
||||
</div>
|
||||
|
||||
@@ -1511,6 +1511,9 @@ def init_db():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from strategy_db import init_strategy_tables
|
||||
|
||||
init_strategy_tables(conn)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -2839,12 +2842,11 @@ def parse_and_compute_trend_pullback_plan(form_dict):
|
||||
return None, "杠杆格式错误"
|
||||
if leverage <= 0 or risk_percent <= 0:
|
||||
return None, "杠杆与风险比例必须大于0"
|
||||
if direction == "long":
|
||||
if not (stop_loss < add_upper):
|
||||
return None, "做多:止损价须低于补仓上沿"
|
||||
else:
|
||||
if not (stop_loss > add_upper):
|
||||
return None, "做空:止损价须高于补仓下沿"
|
||||
from strategy_trend_lib import validate_trend_bounds
|
||||
|
||||
bound_err = validate_trend_bounds(direction, stop_loss, add_upper)
|
||||
if bound_err:
|
||||
return None, bound_err
|
||||
snap = get_available_trading_usdt()
|
||||
if snap is None or snap <= 0:
|
||||
return None, "无法读取合约账户 USDT 可用余额,请检查 API 与账户类型"
|
||||
@@ -2873,10 +2875,21 @@ def parse_and_compute_trend_pullback_plan(form_dict):
|
||||
)
|
||||
if remainder_total is None:
|
||||
remainder_total = 0.0
|
||||
n_legs, leg_json, per_ref = _trend_build_leg_amounts_json(exchange_symbol, remainder_total, TREND_PULLBACK_DCA_LEGS)
|
||||
from strategy_trend_lib import build_grid_prices, build_leg_amounts_json
|
||||
|
||||
ensure_markets_loaded()
|
||||
market = exchange.market(exchange_symbol)
|
||||
min_amt = float((market.get("limits", {}).get("amount", {}) or {}).get("min") or 0)
|
||||
n_legs, leg_json, per_ref = build_leg_amounts_json(
|
||||
exchange_symbol,
|
||||
remainder_total,
|
||||
TREND_PULLBACK_DCA_LEGS,
|
||||
_safe_amount_to_precision,
|
||||
min_amt,
|
||||
)
|
||||
if n_legs <= 0:
|
||||
return None, "剩余计划张数不足以拆出补仓档(低于交易所最小张数),请提高风险比例、放宽止损与补仓区间间距,或减少补仓档数"
|
||||
grid = _trend_build_grid_prices(direction, stop_loss, add_upper, n_legs)
|
||||
grid = build_grid_prices(direction, stop_loss, add_upper, n_legs)
|
||||
if len(grid) != n_legs:
|
||||
return None, "补仓网格生成失败"
|
||||
opened_at = app_now_str()
|
||||
@@ -5278,7 +5291,7 @@ def render_main_page(page="trade"):
|
||||
preview_expires_ms = None
|
||||
trend_preview_expired = False
|
||||
trend_preview_id_arg = ""
|
||||
if page == "trade":
|
||||
if page == "strategy_trend":
|
||||
_trend_cleanup_stale_previews(conn)
|
||||
trend_preview_id_arg = (request.args.get("preview_id") or "").strip()
|
||||
if trend_preview_id_arg:
|
||||
@@ -6182,17 +6195,17 @@ def preview_trend_pullback():
|
||||
if not okp:
|
||||
conn.close()
|
||||
flash(reasonp)
|
||||
return redirect(url_for("trade_page"))
|
||||
return redirect(url_for("strategy_trend_page"))
|
||||
ok_live, reason_live = ensure_exchange_live_ready()
|
||||
if not ok_live:
|
||||
conn.close()
|
||||
flash(reason_live)
|
||||
return redirect(url_for("trade_page"))
|
||||
return redirect(url_for("strategy_trend_page"))
|
||||
payload, err = parse_and_compute_trend_pullback_plan(request.form)
|
||||
if err:
|
||||
conn.close()
|
||||
flash(err)
|
||||
return redirect(url_for("trade_page"))
|
||||
return redirect(url_for("strategy_trend_page"))
|
||||
pid = str(uuid.uuid4())
|
||||
exp_ms = int(time.time() * 1000) + int(TREND_PULLBACK_PREVIEW_TTL_SECONDS) * 1000
|
||||
created = app_now_str()
|
||||
@@ -6231,7 +6244,7 @@ def preview_trend_pullback():
|
||||
conn.commit()
|
||||
conn.close()
|
||||
flash(f"预览已生成,有效期 {TREND_PULLBACK_PREVIEW_TTL_SECONDS} 秒,请核对后点击「确认执行」。")
|
||||
return redirect(url_for("trade_page", preview_id=pid))
|
||||
return redirect(url_for("strategy_trend_page", preview_id=pid))
|
||||
|
||||
|
||||
@app.route("/execute_trend_pullback", methods=["POST"])
|
||||
@@ -6240,7 +6253,7 @@ def execute_trend_pullback():
|
||||
pid = (request.form.get("preview_id") or "").strip()
|
||||
if not pid:
|
||||
flash("缺少预览 ID")
|
||||
return redirect(url_for("trade_page"))
|
||||
return redirect(url_for("strategy_trend_page"))
|
||||
conn = get_db()
|
||||
_trend_cleanup_stale_previews(conn)
|
||||
pr = conn.execute("SELECT * FROM trend_pullback_previews WHERE id=?", (pid,)).fetchone()
|
||||
@@ -6248,30 +6261,30 @@ def execute_trend_pullback():
|
||||
if not pr or int(pr["expires_at_ms"] or 0) < now_ms:
|
||||
conn.close()
|
||||
flash("预览已过期或不存在,请重新生成预览")
|
||||
return redirect(url_for("trade_page"))
|
||||
return redirect(url_for("strategy_trend_page"))
|
||||
okp, reasonp = precheck_trend_pullback_start(conn)
|
||||
if not okp:
|
||||
conn.close()
|
||||
flash(reasonp)
|
||||
return redirect(url_for("trade_page", preview_id=pid))
|
||||
return redirect(url_for("strategy_trend_page", preview_id=pid))
|
||||
ok_live, reason_live = ensure_exchange_live_ready()
|
||||
if not ok_live:
|
||||
conn.close()
|
||||
flash(reason_live)
|
||||
return redirect(url_for("trade_page", preview_id=pid))
|
||||
return redirect(url_for("strategy_trend_page", preview_id=pid))
|
||||
snap_prev = float(pr["snapshot_available_usdt"] or 0)
|
||||
snap_now = get_available_trading_usdt()
|
||||
if snap_now is None or snap_now <= 0:
|
||||
conn.close()
|
||||
flash("无法读取当前合约可用余额,请稍后重试")
|
||||
return redirect(url_for("trade_page", preview_id=pid))
|
||||
return redirect(url_for("strategy_trend_page", preview_id=pid))
|
||||
drift_pct = abs(float(snap_now) - snap_prev) / max(snap_prev, 1e-9) * 100.0
|
||||
if drift_pct > float(TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT):
|
||||
conn.close()
|
||||
flash(
|
||||
f"当前可用余额与预览快照偏差 {drift_pct:.2f}%,超过允许 {TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT}% ,请重新生成预览"
|
||||
)
|
||||
return redirect(url_for("trade_page"))
|
||||
return redirect(url_for("strategy_trend_page"))
|
||||
symbol = pr["symbol"]
|
||||
exchange_symbol = pr["exchange_symbol"]
|
||||
direction = pr["direction"] or "long"
|
||||
@@ -6293,7 +6306,7 @@ def execute_trend_pullback():
|
||||
if live_price is None:
|
||||
conn.close()
|
||||
flash("获取实时价格失败")
|
||||
return redirect(url_for("trade_page", preview_id=pid))
|
||||
return redirect(url_for("strategy_trend_page", preview_id=pid))
|
||||
try:
|
||||
o1 = place_exchange_order(exchange_symbol, direction, first_amt, leverage, stop_loss=None, take_profit=None)
|
||||
fill1 = resolve_order_entry_price(o1, exchange_symbol, live_price)
|
||||
@@ -6301,7 +6314,7 @@ def execute_trend_pullback():
|
||||
except Exception as e:
|
||||
conn.close()
|
||||
flash(friendly_exchange_error(e, available_usdt=snap_now))
|
||||
return redirect(url_for("trade_page", preview_id=pid))
|
||||
return redirect(url_for("strategy_trend_page", preview_id=pid))
|
||||
now = app_now()
|
||||
trading_day = get_trading_day(now)
|
||||
opened_at = app_now_str()
|
||||
@@ -6356,7 +6369,7 @@ def execute_trend_pullback():
|
||||
f"趋势回调已执行:可用余额(执行时){round(snap, 2)}U;计划保证金约 {round(margin_plan, 2)}U;"
|
||||
f"总张数约 {target_amt},首仓 {first_amt},补仓 {n_legs} 档;已挂交易所止损,止盈由程序监控。"
|
||||
)
|
||||
return redirect(url_for("trade_page"))
|
||||
return redirect(url_for("strategy_trend_page"))
|
||||
|
||||
|
||||
@app.route("/cancel_trend_pullback_preview", methods=["POST"])
|
||||
@@ -6373,7 +6386,7 @@ def cancel_trend_pullback_preview():
|
||||
conn.commit()
|
||||
conn.close()
|
||||
flash("已取消预览")
|
||||
return redirect(url_for("trade_page"))
|
||||
return redirect(url_for("strategy_trend_page"))
|
||||
|
||||
|
||||
@app.route("/trend_pullback_breakeven/<int:pid>", methods=["POST"])
|
||||
@@ -6388,7 +6401,7 @@ def trend_pullback_breakeven(pid):
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
flash("保本偏移% 格式无效")
|
||||
return redirect(url_for("trade_page"))
|
||||
return redirect(url_for("strategy_trend_page"))
|
||||
conn = get_db()
|
||||
row = conn.execute(
|
||||
"SELECT * FROM trend_pullback_plans WHERE id=? AND status='active'", (pid,)
|
||||
@@ -6396,7 +6409,7 @@ def trend_pullback_breakeven(pid):
|
||||
if not row:
|
||||
conn.close()
|
||||
flash("未找到运行中的趋势回调计划")
|
||||
return redirect(url_for("trade_page"))
|
||||
return redirect(url_for("strategy_trend_page"))
|
||||
ok, err = apply_trend_pullback_manual_breakeven(conn, row, offset_pct=offset_pct)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -6404,7 +6417,7 @@ def trend_pullback_breakeven(pid):
|
||||
flash("已手动保本:交易所止损已按均价+偏移更新")
|
||||
else:
|
||||
flash(err or "手动保本失败")
|
||||
return redirect(url_for("trade_page"))
|
||||
return redirect(url_for("strategy_trend_page"))
|
||||
|
||||
|
||||
@app.route("/stop_trend_pullback/<int:pid>")
|
||||
@@ -7358,6 +7371,19 @@ except Exception as _hub_err:
|
||||
print(f"[hub_bridge] gate_bot: {_hub_err}")
|
||||
|
||||
|
||||
def strategy_trend_page():
|
||||
return render_main_page("strategy_trend")
|
||||
|
||||
|
||||
from strategy_config import build_strategy_config
|
||||
from strategy_register import attach_strategy_templates, register_strategy_trading
|
||||
|
||||
attach_strategy_templates(app, _REPO_ROOT)
|
||||
_strategy_cfg = build_strategy_config(sys.modules[__name__], trend_enabled=True)
|
||||
_strategy_cfg["render_trend_page"] = login_required(strategy_trend_page)
|
||||
register_strategy_trading(app, _strategy_cfg)
|
||||
|
||||
|
||||
# 启动
|
||||
if __name__ == "__main__":
|
||||
threading.Thread(target=background_task, daemon=True).start()
|
||||
|
||||
@@ -205,6 +205,8 @@
|
||||
</div>
|
||||
<div class="top-nav">
|
||||
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">交易执行</a>
|
||||
<a href="/strategy/trend" class="{% if page == 'strategy_trend' %}active{% endif %}">策略·趋势回调</a>
|
||||
<a href="/strategy/roll" class="{% if page == 'strategy_roll' %}active{% endif %}">策略·顺势加仓</a>
|
||||
<a href="/records" class="{% if page == 'records' %}active{% endif %}">交易记录与复盘</a>
|
||||
<a href="/stats" class="{% if page == 'stats' %}active{% endif %}">统计分析</a>
|
||||
</div>
|
||||
@@ -367,7 +369,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% elif page == 'strategy_trend' %}
|
||||
<div class="card trend-card">
|
||||
<h2 style="margin-bottom:8px">趋势回调策略</h2>
|
||||
<div class="rule-tip">
|
||||
@@ -549,8 +553,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if page == 'records' %}
|
||||
|
||||
@@ -1318,11 +1318,20 @@ def init_db():
|
||||
close_reason TEXT, closed_at TEXT)"""
|
||||
)
|
||||
|
||||
from strategy_db import init_strategy_tables
|
||||
|
||||
init_strategy_tables(conn)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
init_db()
|
||||
|
||||
from strategy_config import build_strategy_config
|
||||
from strategy_register import attach_strategy_templates, register_strategy_trading
|
||||
|
||||
attach_strategy_templates(app, _REPO_ROOT)
|
||||
register_strategy_trading(app, build_strategy_config(sys.modules[__name__]))
|
||||
|
||||
def get_db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
@@ -154,6 +154,8 @@
|
||||
<div class="header"><h1>加密货币|交易监控 + AI复盘一体化</h1></div>
|
||||
<div class="top-nav">
|
||||
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">交易执行</a>
|
||||
<a href="/strategy/trend" class="{% if page == 'strategy_trend' %}active{% endif %}">策略·趋势回调</a>
|
||||
<a href="/strategy/roll" class="{% if page == 'strategy_roll' %}active{% endif %}">策略·顺势加仓</a>
|
||||
<a href="/records" class="{% if page == 'records' %}active{% endif %}">交易记录与复盘</a>
|
||||
<a href="/stats" class="{% if page == 'stats' %}active{% endif %}">统计分析</a>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"""各交易所 app 模块 → strategy_register 配置(统一工厂)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_strategy_config(app_module: Any, *, trend_enabled: bool = False, trend_disabled_note: str = "") -> dict:
|
||||
m = 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"
|
||||
params = {}
|
||||
if hasattr(m, "build_gate_order_params"):
|
||||
params = m.build_gate_order_params(direction, reduce_only=False)
|
||||
return m.exchange.create_order(ex_sym, "limit", side, float(amount), float(price), params or None)
|
||||
|
||||
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
|
||||
|
||||
note = trend_disabled_note or (
|
||||
"趋势回调(自动补仓)请在 Gate 趋势机器人实例使用:/strategy/trend"
|
||||
)
|
||||
return {
|
||||
"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": lambda e: m.friendly_exchange_error(e, available_usdt=m.get_available_trading_usdt())
|
||||
if "friendly_exchange_error" in dir(m)
|
||||
else str(e),
|
||||
"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,
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
"""策略交易相关表结构(各所 crypto.db 共用 schema)。"""
|
||||
|
||||
ROLL_GROUPS_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS roll_groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_monitor_id INTEGER,
|
||||
symbol TEXT NOT NULL,
|
||||
exchange_symbol TEXT,
|
||||
direction TEXT NOT NULL,
|
||||
initial_take_profit REAL,
|
||||
initial_stop_loss REAL,
|
||||
current_stop_loss REAL,
|
||||
risk_percent REAL DEFAULT 2,
|
||||
leg_count INTEGER DEFAULT 0,
|
||||
status TEXT DEFAULT 'active',
|
||||
created_at TEXT,
|
||||
updated_at TEXT
|
||||
)
|
||||
"""
|
||||
|
||||
ROLL_LEGS_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS roll_legs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
roll_group_id INTEGER NOT NULL,
|
||||
leg_index INTEGER NOT NULL,
|
||||
add_mode TEXT NOT NULL,
|
||||
fib_upper REAL,
|
||||
fib_lower REAL,
|
||||
limit_price REAL,
|
||||
fill_price REAL,
|
||||
amount REAL,
|
||||
new_stop_loss REAL,
|
||||
exchange_order_id TEXT,
|
||||
status TEXT DEFAULT 'filled',
|
||||
created_at TEXT,
|
||||
FOREIGN KEY (roll_group_id) REFERENCES roll_groups(id)
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def init_strategy_tables(conn) -> None:
|
||||
conn.execute(ROLL_GROUPS_SQL)
|
||||
conn.execute(ROLL_LEGS_SQL)
|
||||
@@ -0,0 +1,48 @@
|
||||
"""交易所策略适配器接口(各所 app 注入 ccxt 实现)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional, Protocol
|
||||
|
||||
|
||||
class StrategyExchangeAdapter(Protocol):
|
||||
exchange_key: str
|
||||
|
||||
def normalize_symbol(self, raw: str) -> str: ...
|
||||
|
||||
def normalize_exchange_symbol(self, symbol: str) -> str: ...
|
||||
|
||||
def get_mark_price(self, symbol: str) -> Optional[float]: ...
|
||||
|
||||
def get_position(self, exchange_symbol: str, direction: str) -> dict[str, Any]:
|
||||
"""返回 {contracts, entry_price, leverage?}。"""
|
||||
...
|
||||
|
||||
def amount_to_precision(self, exchange_symbol: str, amount: float) -> Optional[float]: ...
|
||||
|
||||
def price_to_precision(self, exchange_symbol: str, price: float) -> Optional[float]: ...
|
||||
|
||||
def market_add(
|
||||
self, exchange_symbol: str, direction: str, amount: float, leverage: int
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
def limit_add(
|
||||
self,
|
||||
exchange_symbol: str,
|
||||
direction: str,
|
||||
amount: float,
|
||||
price: float,
|
||||
leverage: int,
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
def cancel_order(self, exchange_symbol: str, order_id: str) -> None: ...
|
||||
|
||||
def replace_position_tpsl(
|
||||
self,
|
||||
exchange_symbol: str,
|
||||
direction: str,
|
||||
stop_loss: float,
|
||||
take_profit: float,
|
||||
order_monitor_row: Any = None,
|
||||
) -> None: ...
|
||||
|
||||
def ensure_live_ready(self) -> tuple[bool, str]: ...
|
||||
@@ -0,0 +1,4 @@
|
||||
"""Binance USDT-M 永续 — 策略交易交易所适配(见 strategy_config.build_strategy_config)。"""
|
||||
from strategy_exchange_base import StrategyExchangeAdapter
|
||||
|
||||
__all__ = ["StrategyExchangeAdapter"]
|
||||
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
Gate.io USDT 永续 — 策略交易交易所侧能力。
|
||||
|
||||
实现方式:各 Gate 实例 app 通过 strategy_config.build_strategy_config(app_module) 注入
|
||||
ccxt 下单、精度、换 TP/SL;本文件为文档与类型锚点,避免在四个 app 重复实现滚仓公式。
|
||||
"""
|
||||
from strategy_exchange_base import StrategyExchangeAdapter
|
||||
|
||||
__all__ = ["StrategyExchangeAdapter"]
|
||||
@@ -0,0 +1,4 @@
|
||||
"""OKX 永续 — 策略交易交易所适配(见 strategy_config.build_strategy_config)。"""
|
||||
from strategy_exchange_base import StrategyExchangeAdapter
|
||||
|
||||
__all__ = ["StrategyExchangeAdapter"]
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
"""顺势加仓(滚仓):纯计算。人工触发;止盈锁定首仓;斐波仅算限价。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional, Tuple
|
||||
|
||||
from fib_key_monitor_lib import calc_fib_plan, fib_ratio_from_type
|
||||
|
||||
ROLL_MAX_LEGS_LONG = 3
|
||||
ROLL_MAX_LEGS_SHORT = 3
|
||||
FIB_MODES = frozenset({"fib_618", "fib_786"})
|
||||
|
||||
|
||||
def fib_ratio_from_mode(mode: str) -> Optional[float]:
|
||||
m = (mode or "").strip().lower()
|
||||
if m in ("fib_618", "618", "0.618"):
|
||||
return 0.618
|
||||
if m in ("fib_786", "786", "0.786"):
|
||||
return 0.786
|
||||
return None
|
||||
|
||||
|
||||
def fib_limit_entry(direction: str, upper: float, lower: float, mode: str) -> Tuple[Optional[float], Optional[str]]:
|
||||
"""H/L 仅用于计算限价加仓价;多:下沿=止损侧;空:上沿=止损侧。"""
|
||||
ratio = fib_ratio_from_mode(mode)
|
||||
if ratio is None:
|
||||
return None, "斐波档位无效"
|
||||
h, l = float(upper), float(lower)
|
||||
if h <= l:
|
||||
return None, "上沿须大于下沿"
|
||||
direction = (direction or "long").strip().lower()
|
||||
if direction == "short":
|
||||
plan = calc_fib_plan("short", h, l, ratio)
|
||||
else:
|
||||
plan = calc_fib_plan("long", h, l, ratio)
|
||||
if not plan:
|
||||
return None, "无法计算斐波限价"
|
||||
entry, _sl, _tp = plan
|
||||
return float(entry), None
|
||||
|
||||
|
||||
def max_roll_legs(direction: str) -> int:
|
||||
return ROLL_MAX_LEGS_LONG if (direction or "long").strip().lower() == "long" else ROLL_MAX_LEGS_SHORT
|
||||
|
||||
|
||||
def solve_add_amount_for_total_risk(
|
||||
direction: str,
|
||||
qty_existing: float,
|
||||
entry_existing: float,
|
||||
add_price: float,
|
||||
new_stop: float,
|
||||
risk_budget_usdt: float,
|
||||
) -> Tuple[Optional[float], Optional[str]]:
|
||||
"""
|
||||
已知合并后若触及 new_stop 总亏损=risk_budget,反推本次加仓张数 Q2。
|
||||
long: (avg - SL) * (Q1+Q2) = B => Q2 = (B - Q1*(E1-SL)) / (E2-SL)
|
||||
short: (SL - avg) * (Q1+Q2) = B => Q2 = (B - Q1*(SL-E1)) / (SL-E2)
|
||||
"""
|
||||
try:
|
||||
q1 = float(qty_existing)
|
||||
e1 = float(entry_existing)
|
||||
e2 = float(add_price)
|
||||
sl = float(new_stop)
|
||||
b = float(risk_budget_usdt)
|
||||
except (TypeError, ValueError):
|
||||
return None, "参数格式错误"
|
||||
if q1 <= 0 or e1 <= 0 or e2 <= 0 or b <= 0:
|
||||
return None, "持仓或风险预算无效"
|
||||
direction = (direction or "long").strip().lower()
|
||||
if direction == "short":
|
||||
denom = sl - e2
|
||||
numer = b - q1 * (sl - e1)
|
||||
if denom <= 0:
|
||||
return None, "做空:新止损须高于限价加仓价"
|
||||
else:
|
||||
denom = e2 - sl
|
||||
numer = b - q1 * (e1 - sl)
|
||||
if denom <= 0:
|
||||
return None, "做多:新止损须低于限价/市价加仓价"
|
||||
q2 = numer / denom
|
||||
if q2 <= 0:
|
||||
return None, "按当前新止损与总风险%,无需加仓或无法再加(已满足风险上限)"
|
||||
return q2, None
|
||||
|
||||
|
||||
def preview_roll(
|
||||
*,
|
||||
direction: str,
|
||||
symbol: str,
|
||||
qty_existing: float,
|
||||
entry_existing: float,
|
||||
initial_take_profit: float,
|
||||
add_mode: str,
|
||||
new_stop_loss: float,
|
||||
risk_percent: float,
|
||||
capital_base_usdt: float,
|
||||
add_price: Optional[float] = None,
|
||||
fib_upper: Optional[float] = None,
|
||||
fib_lower: Optional[float] = None,
|
||||
legs_done: int = 0,
|
||||
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
||||
direction = (direction or "long").strip().lower()
|
||||
if legs_done >= max_roll_legs(direction):
|
||||
return None, f"{'做多' if direction == 'long' else '做空'}滚仓已达 {max_roll_legs(direction)} 次上限"
|
||||
mode = (add_mode or "market").strip().lower()
|
||||
if mode == "market":
|
||||
if add_price is None or add_price <= 0:
|
||||
return None, "市价加仓需要有效参考价"
|
||||
entry_add = float(add_price)
|
||||
mode_label = "市价"
|
||||
elif mode in FIB_MODES:
|
||||
if fib_upper is None or fib_lower is None:
|
||||
return None, "斐波限价须填写上沿与下沿"
|
||||
entry_add, err = fib_limit_entry(direction, float(fib_upper), float(fib_lower), mode)
|
||||
if err:
|
||||
return None, err
|
||||
mode_label = "斐波0.618" if "618" in mode else "斐波0.786"
|
||||
else:
|
||||
return None, "加仓方式无效"
|
||||
try:
|
||||
sl = float(new_stop_loss)
|
||||
tp = float(initial_take_profit)
|
||||
except (TypeError, ValueError):
|
||||
return None, "止损/止盈格式错误"
|
||||
if sl <= 0 or tp <= 0:
|
||||
return None, "止损与首仓止盈须大于0"
|
||||
if direction == "long":
|
||||
if sl >= entry_add:
|
||||
return None, "做多:新止损须低于加仓价"
|
||||
if tp <= entry_existing:
|
||||
return None, "做多:首仓止盈须高于当前持仓均价参考"
|
||||
else:
|
||||
if sl <= entry_add:
|
||||
return None, "做空:新止损须高于加仓价"
|
||||
if tp >= entry_existing:
|
||||
return None, "做空:首仓止盈须低于当前持仓均价参考"
|
||||
risk_budget = float(capital_base_usdt) * (float(risk_percent) / 100.0)
|
||||
q2_raw, err = solve_add_amount_for_total_risk(
|
||||
direction, qty_existing, entry_existing, entry_add, sl, risk_budget
|
||||
)
|
||||
if err:
|
||||
return None, err
|
||||
q2 = float(q2_raw)
|
||||
new_qty = qty_existing + q2
|
||||
new_avg = (qty_existing * entry_existing + q2 * entry_add) / new_qty
|
||||
if direction == "long":
|
||||
loss_at_sl = (new_avg - sl) * new_qty
|
||||
reward_at_tp = (tp - new_avg) * new_qty
|
||||
else:
|
||||
loss_at_sl = (sl - new_avg) * new_qty
|
||||
reward_at_tp = (new_avg - tp) * new_qty
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"direction": direction,
|
||||
"add_mode": mode,
|
||||
"add_mode_label": mode_label,
|
||||
"add_price": round(entry_add, 10),
|
||||
"new_stop_loss": sl,
|
||||
"initial_take_profit": tp,
|
||||
"risk_percent": float(risk_percent),
|
||||
"risk_budget_usdt": round(risk_budget, 4),
|
||||
"add_amount_raw": q2,
|
||||
"qty_after": new_qty,
|
||||
"avg_entry_after": round(new_avg, 10),
|
||||
"loss_at_sl_usdt": round(loss_at_sl, 4),
|
||||
"reward_at_tp_usdt": round(reward_at_tp, 4),
|
||||
"legs_done": int(legs_done),
|
||||
"leg_index_next": int(legs_done) + 1,
|
||||
"fib_upper": fib_upper,
|
||||
"fib_lower": fib_lower,
|
||||
}, None
|
||||
@@ -0,0 +1,106 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>顺势加仓 · {{ exchange_display }}</title>
|
||||
<style>
|
||||
body{font-family:system-ui,sans-serif;background:#0f1117;color:#e6e8ef;margin:0;padding:16px}
|
||||
.container{max-width:1100px;margin:0 auto}
|
||||
.top-nav{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px}
|
||||
.top-nav a{padding:6px 10px;border:1px solid #304164;border-radius:8px;background:#151a2a;color:#8fc8ff;text-decoration:none}
|
||||
.top-nav a.active{background:#2a3f6c;color:#dbe4ff}
|
||||
.card{background:#151a2a;border:1px solid #2a3150;border-radius:10px;padding:14px;margin-bottom:12px}
|
||||
.form-row{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px}
|
||||
.form-row input,.form-row select{padding:8px 10px;border-radius:6px;border:1px solid #3a4a66;background:#0f1420;color:#eee;min-width:120px}
|
||||
.form-row button{padding:8px 14px;border-radius:8px;border:none;background:#2d6a4f;color:#fff;cursor:pointer}
|
||||
.rule-tip{font-size:.8rem;color:#8892b0;line-height:1.5;margin-bottom:10px}
|
||||
.flash{background:#1f2a44;border:1px solid #3a5a8a;padding:10px;border-radius:8px;margin-bottom:12px}
|
||||
table{width:100%;border-collapse:collapse;font-size:.82rem}
|
||||
th,td{border-bottom:1px solid #2a3150;padding:6px 8px;text-align:left}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>策略交易 · 顺势加仓 <span style="font-size:.85rem;color:#8fc8ff">{{ exchange_display }}</span></h1>
|
||||
<div class="top-nav">
|
||||
<a href="/trade">实盘下单</a>
|
||||
<a href="/strategy/trend">趋势回调</a>
|
||||
<a href="/strategy/roll" class="active">顺势加仓</a>
|
||||
<a href="/records">交易复盘</a>
|
||||
</div>
|
||||
{% with messages = get_flashed_messages() %}{% if messages %}<div class="flash">{{ messages[0] }}</div>{% endif %}{% endwith %}
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">规则说明</h2>
|
||||
<div class="rule-tip">
|
||||
<strong>仅人工加仓</strong>,程序不会自动触发。须先在「实盘下单」有同向持仓。<br>
|
||||
做多最多滚仓 <strong>3</strong> 次;止盈<strong>锁定首仓</strong>不变;每次填写<strong>新统一止损</strong>,总风险%按「合并持仓打到新止损≈账户风险」反推张数。<br>
|
||||
斐波限价:上沿 H、下沿 L 仅用于算 0.618/0.786 加仓价(多:下沿=止损侧;空:上沿=止损侧)。<br>
|
||||
{% if trend_active %}<span style="color:#ff8f8f">当前有运行中的趋势回调计划,请先结束后再滚仓。</span>{% endif %}
|
||||
</div>
|
||||
<form action="{{ url_for('strategy_roll_execute') }}" method="post" class="form-row">
|
||||
<select name="symbol" required>
|
||||
<option value="">选择持仓币种</option>
|
||||
{% for o in monitors %}
|
||||
<option value="{{ o.symbol }}">{{ o.symbol }} {{ '多' if o.direction=='long' else '空' }} #{{ o.id }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="direction">
|
||||
<option value="long">做多</option>
|
||||
<option value="short">做空</option>
|
||||
</select>
|
||||
<select name="add_mode">
|
||||
<option value="market">市价加仓</option>
|
||||
<option value="fib_618">限价 斐波0.618</option>
|
||||
<option value="fib_786">限价 斐波0.786</option>
|
||||
</select>
|
||||
<input name="fib_upper" step="any" placeholder="上沿 H">
|
||||
<input name="fib_lower" step="any" placeholder="下沿 L">
|
||||
<input name="new_stop_loss" step="any" placeholder="新统一止损" required>
|
||||
<input name="risk_percent" type="number" min="0.1" step="0.1" value="{{ default_risk_percent }}" placeholder="总风险%">
|
||||
<button type="submit" {% if trend_active %}disabled{% endif %} onclick="return confirm('确认按预览逻辑实盘加仓并更新止损?')">执行滚仓</button>
|
||||
</form>
|
||||
<p class="rule-tip">建议执行前用浏览器开发者工具 POST <code>/strategy/roll/preview</code> 查看 JSON 预览(或将加入页面内预览按钮)。</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>活跃滚仓组</h3>
|
||||
<table>
|
||||
<tr><th>ID</th><th>币种</th><th>方向</th><th>腿数</th><th>首仓TP</th><th>当前SL</th></tr>
|
||||
{% for g in roll_groups %}
|
||||
<tr>
|
||||
<td>{{ g.id }}</td>
|
||||
<td>{{ g.symbol }}</td>
|
||||
<td>{{ g.direction }}</td>
|
||||
<td>{{ g.leg_count }}</td>
|
||||
<td>{% if price_fmt %}{{ price_fmt(g.symbol, g.initial_take_profit) }}{% else %}{{ g.initial_take_profit }}{% endif %}</td>
|
||||
<td>{% if price_fmt %}{{ price_fmt(g.symbol, g.current_stop_loss) }}{% else %}{{ g.current_stop_loss }}{% endif %}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" style="color:#8892b0">暂无</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>最近滚仓腿</h3>
|
||||
<table>
|
||||
<tr><th>#</th><th>组</th><th>方式</th><th>张数</th><th>新SL</th><th>状态</th></tr>
|
||||
{% for leg in roll_legs %}
|
||||
<tr>
|
||||
<td>{{ leg.leg_index }}</td>
|
||||
<td>{{ leg.roll_group_id }}</td>
|
||||
<td>{{ leg.add_mode }}</td>
|
||||
<td>{{ leg.amount }}</td>
|
||||
<td>{{ leg.new_stop_loss }}</td>
|
||||
<td>{{ leg.status }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" style="color:#8892b0">暂无</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>趋势回调 · {{ exchange_display }}</title>
|
||||
<style>
|
||||
body{font-family:system-ui,sans-serif;background:#0f1117;color:#e6e8ef;padding:24px}
|
||||
a{color:#8fc8ff;margin-right:10px}
|
||||
.box{max-width:640px;background:#151a2a;border:1px solid #2a3150;padding:20px;border-radius:10px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p><a href="/trade">← 实盘下单</a> <a href="/strategy/roll">顺势加仓</a></p>
|
||||
<div class="box">
|
||||
<h1>趋势回调</h1>
|
||||
<p>{{ trend_note }}</p>
|
||||
<p style="color:#8892b0;font-size:.9rem">趋势回调含自动补仓档位,仅在 Gate 趋势机器人(crypto_monitor_gate_bot)实例中运行。</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,193 @@
|
||||
"""趋势回调策略:纯计算与校验(无 ccxt / Flask)。各所 adapter 负责张数精度与下单。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Callable, Optional, Tuple
|
||||
|
||||
AmountPreciseFn = Callable[[str, float], Optional[float]]
|
||||
|
||||
|
||||
def calc_risk_fraction(direction: str, entry_price: float, stop_loss: float) -> Optional[float]:
|
||||
try:
|
||||
entry = float(entry_price)
|
||||
sl = float(stop_loss)
|
||||
if entry <= 0 or sl <= 0:
|
||||
return None
|
||||
if (direction or "long").strip().lower() == "short":
|
||||
risk = sl - entry
|
||||
else:
|
||||
risk = entry - sl
|
||||
if risk <= 0:
|
||||
return None
|
||||
return risk / entry
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def validate_trend_bounds(direction: str, stop_loss: float, add_upper: float) -> Optional[str]:
|
||||
direction = (direction or "long").strip().lower()
|
||||
if direction == "long":
|
||||
if not (float(stop_loss) < float(add_upper)):
|
||||
return "做多:止损价须低于补仓上沿"
|
||||
else:
|
||||
if not (float(stop_loss) > float(add_upper)):
|
||||
return "做空:止损价须高于补仓下沿"
|
||||
return None
|
||||
|
||||
|
||||
def build_grid_prices(direction: str, sl: float, upper: float, n_legs: int) -> list[float]:
|
||||
"""在 (止损, 补仓区间远侧边界) 内生成 n_legs 个触发价(不含端点)。"""
|
||||
sl, upper = float(sl), float(upper)
|
||||
out: list[float] = []
|
||||
if n_legs <= 0:
|
||||
return out
|
||||
direction = (direction or "long").strip().lower()
|
||||
if direction == "long":
|
||||
if upper <= sl:
|
||||
return out
|
||||
span = upper - sl
|
||||
for i in range(1, n_legs + 1):
|
||||
t = i / float(n_legs + 1)
|
||||
out.append(sl + t * span)
|
||||
out.sort(reverse=True)
|
||||
else:
|
||||
if sl <= upper:
|
||||
return out
|
||||
span = sl - upper
|
||||
for i in range(1, n_legs + 1):
|
||||
t = i / float(n_legs + 1)
|
||||
out.append(upper + t * span)
|
||||
out.sort()
|
||||
return [round(p, 10) for p in out]
|
||||
|
||||
|
||||
def pick_dca_legs_and_per_leg(
|
||||
exchange_symbol: str,
|
||||
remainder_total: float,
|
||||
want_legs: int,
|
||||
amount_precise: AmountPreciseFn,
|
||||
min_amount: float = 0.0,
|
||||
) -> Tuple[int, float]:
|
||||
"""按最小张数约束自动减少档位数。返回 (有效档数, 每档参考张数)。"""
|
||||
legs = max(1, int(want_legs))
|
||||
rem = float(remainder_total)
|
||||
min_amt = float(min_amount or 0.0)
|
||||
while legs >= 1:
|
||||
per = rem / legs
|
||||
per_p = amount_precise(exchange_symbol, per)
|
||||
if per_p is None or per_p <= 0:
|
||||
legs -= 1
|
||||
continue
|
||||
if min_amt and per_p + 1e-12 < min_amt:
|
||||
legs -= 1
|
||||
continue
|
||||
return legs, per_p
|
||||
one = amount_precise(exchange_symbol, rem)
|
||||
if one is None or one <= 0:
|
||||
return 0, 0.0
|
||||
return 1, one
|
||||
|
||||
|
||||
def build_leg_amounts_json(
|
||||
exchange_symbol: str,
|
||||
remainder_total: float,
|
||||
want_legs: int,
|
||||
amount_precise: AmountPreciseFn,
|
||||
min_amount: float = 0.0,
|
||||
) -> Tuple[int, str, float]:
|
||||
"""拆分补仓张数 JSON。返回 (档位数, json列表, 每档参考)。"""
|
||||
rem = amount_precise(exchange_symbol, float(remainder_total))
|
||||
if rem is None or rem <= 0:
|
||||
return 0, "[]", 0.0
|
||||
n, _ = pick_dca_legs_and_per_leg(exchange_symbol, rem, want_legs, amount_precise, min_amount)
|
||||
if n <= 0:
|
||||
return 0, "[]", 0.0
|
||||
if n <= 1:
|
||||
one = amount_precise(exchange_symbol, rem)
|
||||
if one is None or one <= 0:
|
||||
return 0, "[]", 0.0
|
||||
return 1, json.dumps([one]), one
|
||||
unit = amount_precise(exchange_symbol, rem / n)
|
||||
if unit is None or unit <= 0:
|
||||
one = amount_precise(exchange_symbol, rem)
|
||||
if one is None or one <= 0:
|
||||
return 0, "[]", 0.0
|
||||
return 1, json.dumps([one]), one
|
||||
parts: list[float] = []
|
||||
acc = 0.0
|
||||
for _ in range(n - 1):
|
||||
parts.append(unit)
|
||||
acc += unit
|
||||
last = amount_precise(exchange_symbol, max(0.0, rem - acc))
|
||||
if last is None or last <= 0:
|
||||
one = amount_precise(exchange_symbol, rem)
|
||||
if one is None or one <= 0:
|
||||
return 0, "[]", 0.0
|
||||
return 1, json.dumps([one]), one
|
||||
parts.append(last)
|
||||
return n, json.dumps(parts), unit
|
||||
|
||||
|
||||
def compute_trend_plan_core(
|
||||
*,
|
||||
direction: str,
|
||||
stop_loss: float,
|
||||
add_upper: float,
|
||||
risk_percent: float,
|
||||
snapshot_usdt: float,
|
||||
leverage: int,
|
||||
live_price: float,
|
||||
target_order_amount: float,
|
||||
exchange_symbol: str,
|
||||
dca_legs: int,
|
||||
amount_precise: AmountPreciseFn,
|
||||
min_amount: float = 0.0,
|
||||
full_margin_buffer_ratio: float = 0.95,
|
||||
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
||||
"""在已有 target_order_amount 时组装预览 payload(张数由调用方 prepare_order_amount 计算)。"""
|
||||
rf = calc_risk_fraction(direction, add_upper, stop_loss)
|
||||
if rf is None or rf <= 0:
|
||||
return None, "止损与补仓区间边界组合无法计算风险比例"
|
||||
risk_budget = float(snapshot_usdt) * (float(risk_percent) / 100.0)
|
||||
notional = risk_budget / rf
|
||||
margin_plan = notional / float(leverage)
|
||||
margin_plan = min(margin_plan, float(snapshot_usdt) * float(full_margin_buffer_ratio))
|
||||
if margin_plan <= 0:
|
||||
return None, "计划保证金过小"
|
||||
first_amt = amount_precise(exchange_symbol, float(target_order_amount) * 0.5)
|
||||
if first_amt is None or first_amt <= 0:
|
||||
return None, "首仓张数过小(低于交易所最小张数),请提高风险比例或杠杆"
|
||||
remainder_total = amount_precise(exchange_symbol, max(0.0, float(target_order_amount) - float(first_amt)))
|
||||
if remainder_total is None:
|
||||
remainder_total = 0.0
|
||||
n_legs, leg_json, per_ref = build_leg_amounts_json(
|
||||
exchange_symbol, remainder_total, dca_legs, amount_precise, min_amount
|
||||
)
|
||||
if n_legs <= 0:
|
||||
return None, "剩余计划张数不足以拆出补仓档,请提高风险比例或放宽止损与补仓区间间距"
|
||||
grid = build_grid_prices(direction, stop_loss, add_upper, n_legs)
|
||||
if len(grid) != n_legs:
|
||||
return None, "补仓网格生成失败"
|
||||
try:
|
||||
leg_list = json.loads(leg_json)
|
||||
except Exception:
|
||||
leg_list = []
|
||||
payload = {
|
||||
"direction": direction,
|
||||
"stop_loss": float(stop_loss),
|
||||
"add_upper": float(add_upper),
|
||||
"risk_percent": float(risk_percent),
|
||||
"snapshot_available_usdt": float(snapshot_usdt),
|
||||
"live_price_ref": float(live_price),
|
||||
"plan_margin_capital": float(margin_plan),
|
||||
"target_order_amount": float(target_order_amount),
|
||||
"first_order_amount": float(first_amt),
|
||||
"remainder_total": float(remainder_total),
|
||||
"dca_legs": int(n_legs),
|
||||
"per_leg_amount": float(per_ref),
|
||||
"grid_prices_json": json.dumps(grid),
|
||||
"leg_amounts_json": leg_json,
|
||||
"grid": grid,
|
||||
"leg_amounts": leg_list,
|
||||
}
|
||||
return payload, None
|
||||
@@ -0,0 +1,118 @@
|
||||
# 策略交易说明
|
||||
|
||||
本文档说明仓库根目录 **共用策略逻辑** 与四个 `crypto_monitor_*` 实例中的 **策略交易** 入口(导航栏「策略·趋势回调」「策略·顺势加仓」)。
|
||||
|
||||
---
|
||||
|
||||
## 一、架构(精简共用)
|
||||
|
||||
```
|
||||
strategy_trend_lib.py # 趋势回调:网格价、补仓拆分、边界校验(纯计算)
|
||||
strategy_roll_lib.py # 顺势加仓:总风险反推、斐波限价、最多 3 腿(纯计算)
|
||||
strategy_db.py # roll_groups / roll_legs 表结构
|
||||
strategy_config.py # 各所 app → 统一回调配置(交易所 API)
|
||||
strategy_register.py # Flask 路由:/strategy/trend、/strategy/roll
|
||||
strategy_exchange_*.py # 适配器说明(实际下单仍走各所 app 的 ccxt)
|
||||
strategy_templates/ # 顺势加仓页、趋势禁用提示页
|
||||
```
|
||||
|
||||
| 层级 | 职责 |
|
||||
|------|------|
|
||||
| **lib** | 不算 ccxt、不写库 |
|
||||
| **config** | 把 `place_exchange_order`、`replace_active_monitor_tpsl_on_exchange` 等接到统一 cfg |
|
||||
| **各所 app** | `.env`、DB、`init_db`、PM2、微信、监控轮询 |
|
||||
|
||||
部署时各实例 `PYTHONPATH` 需包含仓库根目录(`ecosystem.config.cjs` 中 `PYTHONPATH=..`)。
|
||||
|
||||
---
|
||||
|
||||
## 二、导航与页面
|
||||
|
||||
| 路由 | 名称 | 说明 |
|
||||
|------|------|------|
|
||||
| `/strategy/trend` | 趋势回调 | **完整功能仅在 `crypto_monitor_gate_bot`**;其它所显示说明页 |
|
||||
| `/strategy/roll` | 顺势加仓 | **四所均可用**(须已有同向持仓) |
|
||||
| `/trade` | 实盘下单 | 首仓、以损定仓、移动保本(不变) |
|
||||
|
||||
---
|
||||
|
||||
## 三、趋势回调(延续 Gate 趋势机器人逻辑)
|
||||
|
||||
- **位置**:`crypto_monitor_gate_bot` → **策略·趋势回调**(原「交易执行」页内区块已迁出)。
|
||||
- **行为**:与《[crypto_monitor_gate_bot/趋势回调策略说明.md](./crypto_monitor_gate_bot/趋势回调策略说明.md)》一致——预览 → 确认执行 → 首仓 50% + 交易所止损 + 多档 **自动** 市价补仓 + 程序监控止盈。
|
||||
- **共用代码**:`parse_and_compute_trend_pullback_plan` 中网格/拆档已改为调用 `strategy_trend_lib`。
|
||||
- **互斥**:与「机器人下单监控」持仓上限、运行中趋势计划互斥(逻辑未改)。
|
||||
|
||||
其它三所打开 `/strategy/trend` 会提示:请使用 Gate 趋势机器人实例。
|
||||
|
||||
---
|
||||
|
||||
## 四、顺势加仓(滚仓,仅人工)
|
||||
|
||||
### 4.1 原则
|
||||
|
||||
- **禁止自动加仓**;仅页面按钮「执行滚仓」或挂限价单(无价格穿越自动下单)。
|
||||
- **全币种**(与各所合约列表一致)。
|
||||
- **止盈**:全程使用 **首仓** `order_monitors.take_profit`,滚仓不改止盈。
|
||||
- **止损**:每次人工填写 **新统一止损**;成交后调用各所 **先撤后挂** TP/SL(止盈仍为首仓)。
|
||||
- **总风险%**:按「合并持仓 + 新止损」反推本次加仓张数,使触及新止损时亏损约 **账户基数 × 风险%**(默认 2%,可在表单修改)。
|
||||
- **做多**最多滚仓 **3** 次(首仓不计入,仅计 `roll_legs` 已成交次数);做空默认同样 3 次(见 `strategy_roll_lib.ROLL_MAX_LEGS_SHORT`)。
|
||||
|
||||
### 4.2 斐波限价
|
||||
|
||||
- 填写 **上沿 H、下沿 L**(H > L),仅用于计算限价加仓价(与 `fib_key_monitor_lib.calc_fib_plan` 的 **entry** 一致)。
|
||||
- **做多**:下沿 = 结构止损侧;**做空**:上沿 = 结构止损侧。
|
||||
- 可选 **0.618** 或 **0.786**;与关键位自动单的 TP(H/L 对侧)**不同**,滚仓 TP 锁定首仓。
|
||||
|
||||
### 4.3 前置条件
|
||||
|
||||
1. 在 **实盘下单** 已有同 symbol、同方向 **active** `order_monitors`。
|
||||
2. 交易所有同向持仓(读 `get_live_position_contracts`)。
|
||||
3. 无 **active** `trend_pullback_plans`(与趋势回调互斥)。
|
||||
|
||||
### 4.4 数据表(各所 `crypto.db`)
|
||||
|
||||
- `roll_groups`:绑定 `order_monitor_id`、首仓 TP/SL、当前 SL、已滚仓次数。
|
||||
- `roll_legs`:每腿方式(市价 / 斐波0.618 / 斐波0.786)、张数、新 SL、状态(`filled` / `pending`)。
|
||||
|
||||
`init_db()` 时自动 `CREATE TABLE IF NOT EXISTS`(`strategy_db.init_strategy_tables`)。
|
||||
|
||||
### 4.5 操作步骤
|
||||
|
||||
1. 打开 **策略·顺势加仓** `/strategy/roll`。
|
||||
2. 选择持仓币种、方向、加仓方式,填写 H/L(斐波时)、**新统一止损**、总风险%。
|
||||
3. 点击 **执行滚仓**(市价立即加仓并更新止损;限价则挂委托,成交后需再处理止损——当前版本限价 pending 后提示手动同步)。
|
||||
4. 查看页底 **滚仓腿历史**。
|
||||
|
||||
可选:对表单字段 POST `/strategy/roll/preview`(JSON)查看 `strategy_roll_lib.preview_roll` 结果。
|
||||
|
||||
---
|
||||
|
||||
## 五、升级与重启
|
||||
|
||||
```bash
|
||||
cd /opt/crypto_monitor
|
||||
git pull
|
||||
# 四所 PM2 若用到滚仓/趋势 lib,建议重启
|
||||
pm2 restart crypto_binance crypto_gate crypto_gate_bot crypto_okx manual-trading-hub
|
||||
```
|
||||
|
||||
仅改 Python 库、未改模板时,重启对应 Flask 进程即可。
|
||||
|
||||
---
|
||||
|
||||
## 六、相关文档
|
||||
|
||||
| 文档 | 内容 |
|
||||
|------|------|
|
||||
| [crypto_monitor_gate_bot/趋势回调策略说明.md](./crypto_monitor_gate_bot/趋势回调策略说明.md) | 趋势回调细则 |
|
||||
| [manual_trading_hub/使用说明.md](./manual_trading_hub/使用说明.md) | 中控(不含策略交易) |
|
||||
| [fib_key_monitor_lib.py](./fib_key_monitor_lib.py) | 斐波公式共用 |
|
||||
|
||||
---
|
||||
|
||||
## 七、后续可增强(未实现)
|
||||
|
||||
- 滚仓页内嵌预览按钮、限价成交后一键同步止损。
|
||||
- 趋势回调计划逻辑进一步迁入 `strategy_trend_lib` + 各所 adapter 类(当前仅拆出网格/拆档计算)。
|
||||
- Binance / Gate 主站 / OKX 移植趋势回调自动补仓(需复制 `check_trend_pullback_plans` 轮询)。
|
||||
Reference in New Issue
Block a user