接入 SimNow 模拟盘与期货下单、策略及品种推荐功能。
新增 vnpy CTP 桥接、以损定仓/固定张数、趋势回调与滚仓策略、按资金推荐品种及交易风控;模拟盘走 SimNow,实盘预留期货公司配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,690 @@
|
||||
"""期货下单、品种推荐、策略交易路由注册。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Any, Callable
|
||||
|
||||
from flask import flash, jsonify, redirect, render_template, request, url_for
|
||||
|
||||
from contract_specs import calc_position_metrics, get_contract_spec
|
||||
from position_sizing import (
|
||||
MODE_FIXED,
|
||||
MODE_RISK,
|
||||
calc_lots_by_risk,
|
||||
calc_order_tick_metrics,
|
||||
normalize_sizing_mode,
|
||||
)
|
||||
from product_recommend import list_product_recommendations
|
||||
from risk.account_risk_lib import (
|
||||
assert_can_open,
|
||||
get_risk_status,
|
||||
on_mood_journal_freeze,
|
||||
on_user_initiated_close,
|
||||
parse_mood_issues,
|
||||
reduce_cooloff_after_journal,
|
||||
trading_day_label,
|
||||
)
|
||||
from strategy.strategy_db import init_strategy_tables
|
||||
from strategy.strategy_roll_lib import preview_roll
|
||||
from strategy.strategy_snapshot_lib import list_snapshots, save_snapshot
|
||||
from strategy.strategy_trend_lib import compute_trend_plan_futures, trend_dca_level_reached
|
||||
from strategy.strategy_snapshot_lib import STRATEGY_ROLL, STRATEGY_TREND
|
||||
from symbols import ths_to_codes, resolve_main_contract, PRODUCTS
|
||||
from trading_context import (
|
||||
TRADING_MODE_LIVE,
|
||||
TRADING_MODE_SIM,
|
||||
get_account_capital,
|
||||
get_risk_percent,
|
||||
get_sizing_mode,
|
||||
get_trading_mode,
|
||||
trading_mode_label,
|
||||
)
|
||||
from ctp_symbol import ths_to_vnpy_symbol
|
||||
from vnpy_bridge import (
|
||||
ctp_connect,
|
||||
ctp_get_account,
|
||||
ctp_list_positions,
|
||||
ctp_status,
|
||||
execute_order,
|
||||
)
|
||||
|
||||
|
||||
def install_trading(app, *, login_required, get_db, get_setting, set_setting, fetch_price, send_wechat_msg):
|
||||
"""注册交易相关路由。"""
|
||||
|
||||
def _settings_dict() -> dict:
|
||||
return {
|
||||
"trading_mode": get_trading_mode(get_setting),
|
||||
"position_sizing_mode": get_sizing_mode(get_setting),
|
||||
"risk_percent": str(get_risk_percent(get_setting)),
|
||||
}
|
||||
|
||||
def _capital(conn) -> float:
|
||||
return get_account_capital(conn, get_setting)
|
||||
|
||||
def _main_price(product_ths: str):
|
||||
for p in PRODUCTS:
|
||||
if p["ths"] == product_ths:
|
||||
main = resolve_main_contract(p)
|
||||
if not main:
|
||||
return None
|
||||
sym = main.get("ths_code") or ""
|
||||
codes = ths_to_codes(sym)
|
||||
if codes:
|
||||
return fetch_price(sym, codes.get("market_code", ""), codes.get("sina_code", ""))
|
||||
return None
|
||||
|
||||
def _ctp_account(mode: str) -> dict:
|
||||
try:
|
||||
return ctp_get_account(mode)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def _ctp_positions(mode: str) -> list:
|
||||
try:
|
||||
return ctp_list_positions(mode)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _match_ctp_symbol(ctp_sym: str, ths: str) -> bool:
|
||||
a = (ctp_sym or "").lower()
|
||||
b = (ths or "").lower()
|
||||
if a == b:
|
||||
return True
|
||||
try:
|
||||
vnpy_sym, _ = ths_to_vnpy_symbol(ths)
|
||||
return a == vnpy_sym.lower()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@app.route("/trade")
|
||||
@login_required
|
||||
def trade_page():
|
||||
conn = get_db()
|
||||
init_strategy_tables(conn)
|
||||
mode = get_trading_mode(get_setting)
|
||||
ctp_st = ctp_status(mode)
|
||||
capital = _capital(conn)
|
||||
sizing = get_sizing_mode(get_setting)
|
||||
risk = get_risk_status(conn)
|
||||
ctp_acc = _ctp_account(mode) if ctp_st.get("connected") else {}
|
||||
positions = _ctp_positions(mode) if ctp_st.get("connected") else []
|
||||
conn.close()
|
||||
return render_template(
|
||||
"trade.html",
|
||||
trading_mode=mode,
|
||||
trading_mode_label=trading_mode_label(get_setting),
|
||||
sizing_mode=sizing,
|
||||
risk_percent=get_risk_percent(get_setting),
|
||||
capital=capital,
|
||||
risk_status=risk,
|
||||
ctp_status=ctp_st,
|
||||
ctp_account=ctp_acc,
|
||||
ctp_positions=positions,
|
||||
)
|
||||
|
||||
@app.route("/recommend")
|
||||
@login_required
|
||||
def recommend_page():
|
||||
conn = get_db()
|
||||
capital = _capital(conn)
|
||||
conn.close()
|
||||
rows = list_product_recommendations(capital, _main_price)
|
||||
return render_template("recommend.html", capital=capital, rows=rows, trading_mode_label=trading_mode_label(get_setting))
|
||||
|
||||
@app.route("/strategy")
|
||||
@login_required
|
||||
def strategy_page():
|
||||
conn = get_db()
|
||||
init_strategy_tables(conn)
|
||||
capital = _capital(conn)
|
||||
active_trend = conn.execute(
|
||||
"SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC LIMIT 1"
|
||||
).fetchone()
|
||||
monitors = conn.execute(
|
||||
"SELECT * FROM trade_order_monitors WHERE status='active' ORDER BY id DESC"
|
||||
).fetchall()
|
||||
roll_groups = conn.execute(
|
||||
"SELECT * FROM roll_groups WHERE status='active' ORDER BY id DESC"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return render_template(
|
||||
"strategy.html",
|
||||
capital=capital,
|
||||
risk_percent=get_risk_percent(get_setting),
|
||||
sizing_mode=get_sizing_mode(get_setting),
|
||||
active_trend=dict(active_trend) if active_trend else None,
|
||||
monitors=[dict(m) for m in monitors],
|
||||
roll_groups=[dict(g) for g in roll_groups],
|
||||
)
|
||||
|
||||
@app.route("/strategy/records")
|
||||
@login_required
|
||||
def strategy_records_page():
|
||||
conn = get_db()
|
||||
init_strategy_tables(conn)
|
||||
trend, roll = list_snapshots(conn)
|
||||
conn.close()
|
||||
return render_template("strategy_records.html", trend_rows=trend, roll_rows=roll)
|
||||
|
||||
@app.route("/api/trade/quote")
|
||||
@login_required
|
||||
def api_trade_quote():
|
||||
sym = (request.args.get("symbol") or "").strip()
|
||||
lots = request.args.get("lots") or "1"
|
||||
if not sym:
|
||||
return jsonify({"ok": False, "error": "缺少品种"}), 400
|
||||
codes = ths_to_codes(sym)
|
||||
price = fetch_price(sym, codes.get("market_code", "") if codes else "", codes.get("sina_code", "") if codes else "")
|
||||
try:
|
||||
lots_f = max(1, int(float(lots)))
|
||||
except (TypeError, ValueError):
|
||||
lots_f = 1
|
||||
metrics = calc_order_tick_metrics(sym, lots_f, price)
|
||||
spec = get_contract_spec(sym)
|
||||
name = codes.get("name", sym) if codes else sym
|
||||
pos_long = pos_short = 0
|
||||
mode = get_trading_mode(get_setting)
|
||||
ctp_st = ctp_status(mode)
|
||||
if ctp_st.get("connected"):
|
||||
for p in _ctp_positions(mode):
|
||||
if not _match_ctp_symbol(p.get("symbol", ""), sym):
|
||||
continue
|
||||
if p["direction"] == "long":
|
||||
pos_long = int(p["lots"])
|
||||
else:
|
||||
pos_short = int(p["lots"])
|
||||
max_open = int(_capital(get_db()) / (metrics["margin_per_lot"] or 1)) if metrics.get("margin_per_lot") else 0
|
||||
return jsonify({
|
||||
"ok": True,
|
||||
"symbol": sym,
|
||||
"name": name,
|
||||
"price": price,
|
||||
"lots": lots_f,
|
||||
"metrics": metrics,
|
||||
"exchange": codes.get("exchange", "") if codes else "",
|
||||
"pos_long": pos_long,
|
||||
"pos_short": pos_short,
|
||||
"max_open_long": max_open,
|
||||
"max_open_short": max_open,
|
||||
"footer_text": (
|
||||
f"*{name} 每手{spec['mult']}吨/点 最小变动{metrics['tick_size']} "
|
||||
f"每跳{metrics['tick_value_per_lot']}元/手×{lots_f}={metrics['tick_value_total']}元 "
|
||||
f"精度{metrics['price_precision']}位小数"
|
||||
),
|
||||
})
|
||||
|
||||
@app.route("/api/trade/preview", methods=["POST"])
|
||||
@login_required
|
||||
def api_trade_preview():
|
||||
d = request.get_json(silent=True) or {}
|
||||
sym = (d.get("symbol") or "").strip()
|
||||
direction = (d.get("direction") or "long").strip().lower()
|
||||
try:
|
||||
entry = float(d.get("entry") or d.get("price") or 0)
|
||||
sl = float(d.get("stop_loss") or 0)
|
||||
tp = float(d.get("take_profit") or 0)
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({"ok": False, "error": "价格参数无效"}), 400
|
||||
conn = get_db()
|
||||
capital = _capital(conn)
|
||||
conn.close()
|
||||
sizing = get_sizing_mode(get_setting)
|
||||
if sizing == MODE_RISK:
|
||||
lots, err = calc_lots_by_risk(entry, sl, direction, capital, get_risk_percent(get_setting), sym)
|
||||
if err:
|
||||
return jsonify({"ok": False, "error": err}), 400
|
||||
else:
|
||||
try:
|
||||
lots = max(1, int(d.get("lots") or 1))
|
||||
except (TypeError, ValueError):
|
||||
lots = 1
|
||||
metrics = calc_position_metrics(direction, entry, sl, tp, lots, entry, capital, sym)
|
||||
tick = calc_order_tick_metrics(sym, lots, entry)
|
||||
return jsonify({"ok": True, "lots": lots, "sizing_mode": sizing, "metrics": metrics, "tick": tick, "capital": capital})
|
||||
|
||||
@app.route("/api/trade/order", methods=["POST"])
|
||||
@login_required
|
||||
def api_trade_order():
|
||||
d = request.get_json(silent=True) or {}
|
||||
sym = (d.get("symbol") or "").strip()
|
||||
offset = (d.get("offset") or "open").strip().lower()
|
||||
direction = (d.get("direction") or "long").strip().lower()
|
||||
try:
|
||||
lots = max(1, int(d.get("lots") or 1))
|
||||
price = float(d.get("price") or 0)
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({"ok": False, "error": "手数或价格无效"}), 400
|
||||
if not sym or price <= 0:
|
||||
return jsonify({"ok": False, "error": "品种或价格无效"}), 400
|
||||
conn = get_db()
|
||||
init_strategy_tables(conn)
|
||||
if offset.startswith("open"):
|
||||
err = assert_can_open(conn)
|
||||
if err:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": err}), 403
|
||||
mode = get_trading_mode(get_setting)
|
||||
sizing = get_sizing_mode(get_setting)
|
||||
if offset.startswith("open") and sizing == MODE_RISK:
|
||||
sl = float(d.get("stop_loss") or 0)
|
||||
if sl <= 0:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": "以损定仓模式须填写止损价"}), 400
|
||||
lots_calc, err = calc_lots_by_risk(price, sl, direction, _capital(conn), get_risk_percent(get_setting), sym)
|
||||
if err:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": err}), 400
|
||||
lots = lots_calc or lots
|
||||
try:
|
||||
result = execute_order(
|
||||
conn,
|
||||
mode=mode,
|
||||
offset=offset,
|
||||
symbol=sym,
|
||||
direction=direction,
|
||||
lots=lots,
|
||||
price=price,
|
||||
settings=_settings_dict(),
|
||||
)
|
||||
if offset.startswith("open"):
|
||||
sl = d.get("stop_loss")
|
||||
tp = d.get("take_profit")
|
||||
codes = ths_to_codes(sym)
|
||||
conn.execute(
|
||||
"""INSERT INTO trade_order_monitors (
|
||||
symbol, symbol_name, market_code, direction, lots, entry_price,
|
||||
stop_loss, take_profit, open_time, monitor_type, status
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?, 'active')""",
|
||||
(
|
||||
sym,
|
||||
codes.get("name", sym) if codes else sym,
|
||||
codes.get("market_code", "") if codes else "",
|
||||
direction,
|
||||
lots,
|
||||
price,
|
||||
float(sl) if sl else None,
|
||||
float(tp) if tp else None,
|
||||
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"manual",
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
send_wechat_msg(f"{trading_mode_label(get_setting)} {offset} {sym} {direction} {lots}手 @{price}")
|
||||
conn.close()
|
||||
return jsonify({"ok": True, "result": result, "lots": lots})
|
||||
except ValueError as exc:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": str(exc)}), 400
|
||||
except Exception as exc:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": str(exc)}), 500
|
||||
|
||||
@app.route("/api/ctp/connect", methods=["POST"])
|
||||
@login_required
|
||||
def api_ctp_connect():
|
||||
mode = get_trading_mode(get_setting)
|
||||
force = bool((request.get_json(silent=True) or {}).get("force"))
|
||||
try:
|
||||
st = ctp_connect(mode, force=force)
|
||||
acc = _ctp_account(mode)
|
||||
return jsonify({"ok": True, "status": st, "account": acc})
|
||||
except Exception as exc:
|
||||
st = ctp_status(mode)
|
||||
return jsonify({"ok": False, "error": str(exc), "status": st}), 400
|
||||
|
||||
@app.route("/api/ctp/status")
|
||||
@login_required
|
||||
def api_ctp_status():
|
||||
mode = get_trading_mode(get_setting)
|
||||
st = ctp_status(mode)
|
||||
acc = _ctp_account(mode) if st.get("connected") else {}
|
||||
return jsonify({"ok": True, "status": st, "account": acc})
|
||||
|
||||
@app.route("/api/account_snapshot")
|
||||
@login_required
|
||||
def api_account_snapshot():
|
||||
conn = get_db()
|
||||
init_strategy_tables(conn)
|
||||
mode = get_trading_mode(get_setting)
|
||||
ctp_st = ctp_status(mode)
|
||||
capital = _capital(conn)
|
||||
risk = get_risk_status(conn)
|
||||
ctp_acc = _ctp_account(mode) if ctp_st.get("connected") else {}
|
||||
positions = _ctp_positions(mode) if ctp_st.get("connected") else []
|
||||
conn.close()
|
||||
return jsonify({
|
||||
"capital": capital,
|
||||
"trading_mode": mode,
|
||||
"trading_mode_label": trading_mode_label(get_setting),
|
||||
"sizing_mode": get_sizing_mode(get_setting),
|
||||
"risk_status": risk,
|
||||
"ctp_status": ctp_st,
|
||||
"ctp_account": ctp_acc,
|
||||
"positions": positions,
|
||||
})
|
||||
|
||||
@app.route("/api/recommend/list")
|
||||
@login_required
|
||||
def api_recommend_list():
|
||||
conn = get_db()
|
||||
capital = _capital(conn)
|
||||
conn.close()
|
||||
return jsonify({"ok": True, "capital": capital, "rows": list_product_recommendations(capital, _main_price)})
|
||||
|
||||
@app.route("/api/strategy/trend/preview", methods=["POST"])
|
||||
@login_required
|
||||
def api_trend_preview():
|
||||
d = request.get_json(silent=True) or {}
|
||||
sym = (d.get("symbol") or "").strip()
|
||||
conn = get_db()
|
||||
if conn.execute("SELECT id FROM trend_pullback_plans WHERE status='active'").fetchone():
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": "已有运行中趋势计划"}), 400
|
||||
capital = _capital(conn)
|
||||
codes = ths_to_codes(sym)
|
||||
price = fetch_price(sym, codes.get("market_code", "") if codes else "", codes.get("sina_code", "") if codes else "")
|
||||
conn.close()
|
||||
if not price:
|
||||
return jsonify({"ok": False, "error": "无法获取现价"}), 400
|
||||
plan, err = compute_trend_plan_futures(
|
||||
direction=d.get("direction") or "long",
|
||||
stop_loss=float(d.get("stop_loss") or 0),
|
||||
add_upper=float(d.get("add_upper") or 0),
|
||||
take_profit=float(d.get("take_profit") or 0),
|
||||
risk_percent=float(d.get("risk_percent") or get_risk_percent(get_setting)),
|
||||
capital=capital,
|
||||
live_price=price,
|
||||
ths_code=sym,
|
||||
dca_legs=int(d.get("dca_legs") or 5),
|
||||
)
|
||||
if err:
|
||||
return jsonify({"ok": False, "error": err}), 400
|
||||
return jsonify({"ok": True, "plan": plan})
|
||||
|
||||
@app.route("/api/strategy/trend/execute", methods=["POST"])
|
||||
@login_required
|
||||
def api_trend_execute():
|
||||
d = request.get_json(silent=True) or {}
|
||||
sym = (d.get("symbol") or "").strip()
|
||||
conn = get_db()
|
||||
init_strategy_tables(conn)
|
||||
err = assert_can_open(conn)
|
||||
if err:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": err}), 403
|
||||
capital = _capital(conn)
|
||||
codes = ths_to_codes(sym)
|
||||
price = fetch_price(sym, codes.get("market_code", "") if codes else "", codes.get("sina_code", "") if codes else "")
|
||||
plan, perr = compute_trend_plan_futures(
|
||||
direction=d.get("direction") or "long",
|
||||
stop_loss=float(d.get("stop_loss") or 0),
|
||||
add_upper=float(d.get("add_upper") or 0),
|
||||
take_profit=float(d.get("take_profit") or 0),
|
||||
risk_percent=float(d.get("risk_percent") or get_risk_percent(get_setting)),
|
||||
capital=capital,
|
||||
live_price=price or float(d.get("live_price") or 0),
|
||||
ths_code=sym,
|
||||
)
|
||||
if perr:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": perr}), 400
|
||||
mode = get_trading_mode(get_setting)
|
||||
try:
|
||||
execute_order(
|
||||
conn, mode=mode, offset="open", symbol=sym,
|
||||
direction=plan["direction"], lots=plan["first_lots"], price=price, settings=_settings_dict(),
|
||||
)
|
||||
except ValueError as exc:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": str(exc)}), 400
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
cur = conn.execute(
|
||||
"""INSERT INTO trend_pullback_plans (
|
||||
status, symbol, symbol_name, direction, stop_loss, add_upper, take_profit,
|
||||
risk_percent, capital_snapshot, plan_margin, target_lots, first_lots, remainder_lots,
|
||||
dca_legs, leg_amounts_json, grid_prices_json, first_order_done, avg_entry_price,
|
||||
lots_open, opened_at
|
||||
) VALUES ('active',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?,?)""",
|
||||
(
|
||||
sym, codes.get("name", sym) if codes else sym, plan["direction"],
|
||||
plan["stop_loss"], plan["add_upper"], plan["take_profit"],
|
||||
plan["risk_percent"], plan["capital_snapshot"], plan["plan_margin"],
|
||||
plan["target_lots"], plan["first_lots"], plan["remainder_lots"],
|
||||
plan["dca_legs"], plan["leg_amounts_json"], plan["grid_prices_json"],
|
||||
price, plan["first_lots"], now,
|
||||
),
|
||||
)
|
||||
plan_id = cur.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
send_wechat_msg(f"趋势回调首仓 {sym} {plan['first_lots']}手")
|
||||
return jsonify({"ok": True, "plan_id": plan_id, "plan": plan})
|
||||
|
||||
@app.route("/api/strategy/roll/preview", methods=["POST"])
|
||||
@login_required
|
||||
def api_roll_preview():
|
||||
d = request.get_json(silent=True) or {}
|
||||
conn = get_db()
|
||||
mon_id = int(d.get("monitor_id") or 0)
|
||||
mon = conn.execute("SELECT * FROM trade_order_monitors WHERE id=? AND status='active'", (mon_id,)).fetchone()
|
||||
conn.close()
|
||||
if not mon:
|
||||
return jsonify({"ok": False, "error": "无有效持仓监控"}), 400
|
||||
sym = mon["symbol"]
|
||||
spec = get_contract_spec(sym)
|
||||
capital = _capital(get_db())
|
||||
preview, err = preview_roll(
|
||||
direction=mon["direction"],
|
||||
symbol=sym,
|
||||
qty_existing=float(mon["lots"]),
|
||||
entry_existing=float(mon["entry_price"]),
|
||||
initial_take_profit=float(mon["take_profit"] or 0),
|
||||
add_mode=d.get("add_mode") or "market",
|
||||
new_stop_loss=float(d.get("new_stop_loss") or 0),
|
||||
risk_percent=float(d.get("risk_percent") or 2),
|
||||
capital_base=capital,
|
||||
mult=spec["mult"],
|
||||
add_price=float(d.get("add_price") or mon["entry_price"]),
|
||||
fib_upper=d.get("fib_upper"),
|
||||
fib_lower=d.get("fib_lower"),
|
||||
legs_done=int(d.get("legs_done") or 0),
|
||||
)
|
||||
if err:
|
||||
return jsonify({"ok": False, "error": err}), 400
|
||||
return jsonify({"ok": True, "preview": preview})
|
||||
|
||||
@app.route("/api/strategy/roll/execute", methods=["POST"])
|
||||
@login_required
|
||||
def api_roll_execute():
|
||||
d = request.get_json(silent=True) or {}
|
||||
conn = get_db()
|
||||
init_strategy_tables(conn)
|
||||
mon_id = int(d.get("monitor_id") or 0)
|
||||
mon = conn.execute("SELECT * FROM trade_order_monitors WHERE id=? AND status='active'", (mon_id,)).fetchone()
|
||||
if not mon:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": "无有效持仓监控"}), 400
|
||||
if conn.execute("SELECT id FROM trend_pullback_plans WHERE status='active'").fetchone():
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": "趋势回调运行中,不可滚仓"}), 400
|
||||
sym = mon["symbol"]
|
||||
spec = get_contract_spec(sym)
|
||||
capital = _capital(conn)
|
||||
prev, err = preview_roll(
|
||||
direction=mon["direction"],
|
||||
symbol=sym,
|
||||
qty_existing=float(mon["lots"]),
|
||||
entry_existing=float(mon["entry_price"]),
|
||||
initial_take_profit=float(mon["take_profit"] or 0),
|
||||
add_mode=d.get("add_mode") or "market",
|
||||
new_stop_loss=float(d.get("new_stop_loss") or 0),
|
||||
risk_percent=float(d.get("risk_percent") or 2),
|
||||
capital_base=capital,
|
||||
mult=spec["mult"],
|
||||
add_price=float(d.get("add_price") or mon["entry_price"]),
|
||||
)
|
||||
if err:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": err}), 400
|
||||
price = float(prev["add_price"])
|
||||
mode = get_trading_mode(get_setting)
|
||||
try:
|
||||
execute_order(
|
||||
conn, mode=mode, offset="open", symbol=sym,
|
||||
direction=mon["direction"], lots=int(prev["add_lots"]), price=price, settings=_settings_dict(),
|
||||
)
|
||||
except ValueError as exc:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": str(exc)}), 400
|
||||
new_lots = int(mon["lots"]) + int(prev["add_lots"])
|
||||
new_avg = prev["avg_entry_after"]
|
||||
new_sl = prev["new_stop_loss"]
|
||||
conn.execute(
|
||||
"UPDATE trade_order_monitors SET lots=?, entry_price=?, stop_loss=? WHERE id=?",
|
||||
(new_lots, new_avg, new_sl, mon_id),
|
||||
)
|
||||
grp = conn.execute(
|
||||
"SELECT * FROM roll_groups WHERE order_monitor_id=? AND status='active'",
|
||||
(mon_id,),
|
||||
).fetchone()
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
if grp:
|
||||
gid = grp["id"]
|
||||
leg_n = int(grp["leg_count"] or 0) + 1
|
||||
conn.execute(
|
||||
"UPDATE roll_groups SET leg_count=?, current_stop_loss=?, updated_at=? WHERE id=?",
|
||||
(leg_n, new_sl, now, gid),
|
||||
)
|
||||
else:
|
||||
cur = conn.execute(
|
||||
"""INSERT INTO roll_groups (
|
||||
order_monitor_id, symbol, direction, initial_take_profit, initial_stop_loss,
|
||||
current_stop_loss, risk_percent, leg_count, status, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,1,'active',?,?)""",
|
||||
(mon_id, sym, mon["direction"], mon["take_profit"], mon["stop_loss"], new_sl,
|
||||
float(d.get("risk_percent") or 2), now, now),
|
||||
)
|
||||
gid = cur.lastrowid
|
||||
leg_n = 1
|
||||
conn.execute(
|
||||
"""INSERT INTO roll_legs (roll_group_id, leg_index, add_mode, fill_price, lots, new_stop_loss, status, created_at)
|
||||
VALUES (?,?,?,?,?,?, 'filled', ?)""",
|
||||
(gid, leg_n, d.get("add_mode") or "market", price, int(prev["add_lots"]), new_sl, now),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({"ok": True, "preview": prev})
|
||||
|
||||
@app.route("/api/strategy/trend/stop", methods=["POST"])
|
||||
@login_required
|
||||
def api_trend_stop():
|
||||
d = request.get_json(silent=True) or {}
|
||||
plan_id = int(d.get("plan_id") or 0)
|
||||
conn = get_db()
|
||||
plan = conn.execute("SELECT * FROM trend_pullback_plans WHERE id=? AND status='active'", (plan_id,)).fetchone()
|
||||
if not plan:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": "计划不存在"}), 404
|
||||
mode = get_trading_mode(get_setting)
|
||||
price = fetch_price(plan["symbol"]) or float(plan["avg_entry_price"] or 0)
|
||||
try:
|
||||
if int(plan["lots_open"] or 0) > 0:
|
||||
execute_order(
|
||||
conn, mode=mode, offset="close", symbol=plan["symbol"],
|
||||
direction=plan["direction"], lots=int(plan["lots_open"]), price=price, settings=_settings_dict(),
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
conn.execute(
|
||||
"UPDATE trend_pullback_plans SET status='stopped_manual', message=?, opened_at=opened_at WHERE id=?",
|
||||
("手动结束", plan_id),
|
||||
)
|
||||
save_snapshot(
|
||||
conn, strategy_type=STRATEGY_TREND, source_id=plan_id,
|
||||
symbol=plan["symbol"], direction=plan["direction"], result_label="手动结束",
|
||||
payload=dict(plan), opened_at=plan["opened_at"] or "",
|
||||
)
|
||||
on_user_initiated_close(conn, trading_day=trading_day_label())
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({"ok": True})
|
||||
|
||||
def check_trend_plans(app_ref):
|
||||
"""后台:趋势补仓与止盈。"""
|
||||
conn = get_db()
|
||||
init_strategy_tables(conn)
|
||||
rows = conn.execute("SELECT * FROM trend_pullback_plans WHERE status='active'").fetchall()
|
||||
mode = get_trading_mode(get_setting)
|
||||
for plan in rows:
|
||||
sym = plan["symbol"]
|
||||
price = fetch_price(sym)
|
||||
if not price:
|
||||
continue
|
||||
direction = plan["direction"]
|
||||
tp = float(plan["take_profit"] or 0)
|
||||
if tp > 0:
|
||||
hit_tp = (direction == "long" and price >= tp) or (direction == "short" and price <= tp)
|
||||
if hit_tp:
|
||||
try:
|
||||
execute_order(
|
||||
conn, mode=mode, offset="close", symbol=sym, direction=direction,
|
||||
lots=int(plan["lots_open"] or 0), price=price, settings=_settings_dict(),
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
conn.execute(
|
||||
"UPDATE trend_pullback_plans SET status='stopped_tp', message=? WHERE id=?",
|
||||
("程序止盈", plan["id"]),
|
||||
)
|
||||
save_snapshot(
|
||||
conn, strategy_type=STRATEGY_TREND, source_id=plan["id"],
|
||||
symbol=sym, direction=direction, result_label="止盈",
|
||||
payload=dict(plan), opened_at=plan["opened_at"] or "",
|
||||
)
|
||||
send_wechat_msg(f"趋势回调止盈 {sym}")
|
||||
continue
|
||||
try:
|
||||
grid = json.loads(plan["grid_prices_json"] or "[]")
|
||||
legs = json.loads(plan["leg_amounts_json"] or "[]")
|
||||
except Exception:
|
||||
grid, legs = [], []
|
||||
done = int(plan["legs_done"] or 0)
|
||||
if done < len(grid) and done < len(legs):
|
||||
level = float(grid[done])
|
||||
if trend_dca_level_reached(direction, price, level):
|
||||
add_lots = int(legs[done])
|
||||
try:
|
||||
execute_order(
|
||||
conn, mode=mode, offset="open", symbol=sym, direction=direction,
|
||||
lots=add_lots, price=price, settings=_settings_dict(),
|
||||
)
|
||||
new_open = int(plan["lots_open"] or 0) + add_lots
|
||||
old_avg = float(plan["avg_entry_price"] or price)
|
||||
new_avg = (old_avg * int(plan["lots_open"] or 0) + price * add_lots) / new_open if new_open else price
|
||||
conn.execute(
|
||||
"""UPDATE trend_pullback_plans SET legs_done=?, lots_open=?, avg_entry_price=? WHERE id=?""",
|
||||
(done + 1, new_open, new_avg, plan["id"]),
|
||||
)
|
||||
send_wechat_msg(f"趋势回调补仓 {sym} +{add_lots}手 @档位{done+1}")
|
||||
except ValueError:
|
||||
pass
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
app._check_trend_plans = check_trend_plans
|
||||
|
||||
@app.route("/settings/trading", methods=["POST"])
|
||||
@login_required
|
||||
def settings_trading_post():
|
||||
return redirect(url_for("settings"))
|
||||
|
||||
def hook_review_mood(conn, behavior_tags: str, exit_trigger: str, exit_supplement: str):
|
||||
if parse_mood_issues(behavior_tags):
|
||||
on_mood_journal_freeze(conn, trading_day=trading_day_label())
|
||||
if (exit_trigger or "").strip() == "手动平仓" and (exit_supplement or "").strip():
|
||||
reduce_cooloff_after_journal(conn, trading_day=trading_day_label())
|
||||
|
||||
app._risk_review_hook = hook_review_mood
|
||||
Reference in New Issue
Block a user