增加策略交易

This commit is contained in:
dekun
2026-05-23 10:48:50 +08:00
parent ee5dc614e0
commit 103615d7a9
21 changed files with 1278 additions and 29 deletions
+52 -26
View File
@@ -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()