你的说明
This commit is contained in:
+480
-57
@@ -127,6 +127,9 @@ BTC_LEVERAGE = int(os.getenv("BTC_LEVERAGE", "10"))
|
||||
ALT_LEVERAGE = int(os.getenv("ALT_LEVERAGE", "5"))
|
||||
# 交易日滚动与「可开仓」整点:按应用本地时区 wall clock(默认北京时间 UTC+8)
|
||||
TRADING_DAY_RESET_HOUR = int(os.getenv("TRADING_DAY_RESET_HOUR", "8"))
|
||||
TRADING_DAY_RESET_OPEN_GUARD_ENABLED = os.getenv(
|
||||
"TRADING_DAY_RESET_OPEN_GUARD_ENABLED", "true"
|
||||
).lower() in ("1", "true", "yes", "on")
|
||||
APP_TIMEZONE = os.getenv("APP_TIMEZONE", "Asia/Shanghai")
|
||||
|
||||
|
||||
@@ -146,6 +149,7 @@ OKX_API_SECRET = os.getenv("OKX_API_SECRET", "")
|
||||
OKX_API_PASSPHRASE = os.getenv("OKX_API_PASSPHRASE", "")
|
||||
OKX_TD_MODE = os.getenv("OKX_TD_MODE", "cross")
|
||||
OKX_POS_MODE = os.getenv("OKX_POS_MODE", "hedge")
|
||||
EXCHANGE_DISPLAY_NAME = (os.getenv("EXCHANGE_DISPLAY_NAME") or "OKX").strip() or "OKX"
|
||||
BALANCE_REFRESH_SECONDS = int(os.getenv("BALANCE_REFRESH_SECONDS", "60"))
|
||||
PRICE_REFRESH_SECONDS = int(os.getenv("PRICE_REFRESH_SECONDS", "5"))
|
||||
KEY_ALERT_MAX_TIMES = int(os.getenv("KEY_ALERT_MAX_TIMES", "3"))
|
||||
@@ -183,7 +187,16 @@ KEY_MONITOR_ALERT_ONLY_TYPES = frozenset({"关键阻力位", "关键支撑位"})
|
||||
KEY_AUTO_MIN_PLANNED_RR = float(os.getenv("KEY_AUTO_MIN_PLANNED_RR", "1.5"))
|
||||
KEY_STOP_OUTSIDE_BREAKOUT_PCT = float(os.getenv("KEY_STOP_OUTSIDE_BREAKOUT_PCT", "0.5"))
|
||||
KEY_TREND_STOP_OUTSIDE_PCT = float(os.getenv("KEY_TREND_STOP_OUTSIDE_PCT", "1"))
|
||||
KEY_DAILY_VOLUME_RANK_MAX = int(os.getenv("KEY_DAILY_VOLUME_RANK_MAX", "30"))
|
||||
KEY_DAILY_VOLUME_RANK_MAX = max(1, int(os.getenv("KEY_DAILY_VOLUME_RANK_MAX", "30")))
|
||||
|
||||
MANUAL_MIN_PLANNED_RR = float(os.getenv("MANUAL_MIN_PLANNED_RR", "1.4"))
|
||||
MAX_ACTIVE_POSITIONS = max(1, int(os.getenv("MAX_ACTIVE_POSITIONS", "1")))
|
||||
KEY_VOLUME_MA_BARS = max(1, int(os.getenv("KEY_VOLUME_MA_BARS", "20")))
|
||||
KEY_VOLUME_RATIO_MIN = float(os.getenv("KEY_VOLUME_RATIO_MIN", "1.3"))
|
||||
KEY_BREAKOUT_AMP_MIN_PCT = float(os.getenv("KEY_BREAKOUT_AMP_MIN_PCT", "0.03"))
|
||||
KEY_BREAKOUT_AMP_MAX_PCT = float(os.getenv("KEY_BREAKOUT_AMP_MAX_PCT", "0.5"))
|
||||
KEY_CONFIRM_BREAKOUT_BAR = int(os.getenv("KEY_CONFIRM_BREAKOUT_BAR", "-2"))
|
||||
KEY_CONFIRM_BAR = int(os.getenv("KEY_CONFIRM_BAR", "-1"))
|
||||
KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT = os.getenv("KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT", "true").lower() in (
|
||||
"1",
|
||||
"true",
|
||||
@@ -1753,6 +1766,18 @@ def format_price_for_symbol(symbol, value):
|
||||
return text.rstrip("0").rstrip(".") if "." in text else text
|
||||
|
||||
|
||||
FUNDS_DECIMALS = 2
|
||||
|
||||
|
||||
def format_funds_u(value):
|
||||
if value in (None, ""):
|
||||
return "-"
|
||||
try:
|
||||
return f"{float(value):.{FUNDS_DECIMALS}f}"
|
||||
except (TypeError, ValueError):
|
||||
return str(value)
|
||||
|
||||
|
||||
def format_hold_minutes(minutes):
|
||||
if not minutes:
|
||||
return "0分钟"
|
||||
@@ -2194,13 +2219,19 @@ def auto_transfer_once_per_day():
|
||||
)
|
||||
|
||||
|
||||
def trading_day_reset_allows_new_open(now):
|
||||
if not TRADING_DAY_RESET_OPEN_GUARD_ENABLED:
|
||||
return True
|
||||
return now.hour >= TRADING_DAY_RESET_HOUR
|
||||
|
||||
|
||||
def precheck_risk(conn, symbol, direction):
|
||||
now = app_now()
|
||||
if now.hour < TRADING_DAY_RESET_HOUR:
|
||||
if not trading_day_reset_allows_new_open(now):
|
||||
return False, f"北京时间 {TRADING_DAY_RESET_HOUR}:00 前不允许持仓"
|
||||
active_count = conn.execute("SELECT COUNT(*) FROM order_monitors WHERE status='active'").fetchone()[0]
|
||||
if active_count > 0:
|
||||
return False, "一次只能持有一个仓位"
|
||||
active_count = get_active_position_count(conn)
|
||||
if active_count >= MAX_ACTIVE_POSITIONS:
|
||||
return False, f"已达最大持仓数({active_count}/{MAX_ACTIVE_POSITIONS})"
|
||||
if direction not in ("long", "short"):
|
||||
return False, "方向必须为 long 或 short"
|
||||
if symbol.upper().startswith("BTC") or symbol.upper().startswith("ETH"):
|
||||
@@ -2393,6 +2424,210 @@ def _okx_place_tp_sl_orders(exchange_symbol, direction, amount, stop_loss, take_
|
||||
exchange.create_order(exchange_symbol, "market", close_side, amt, None, params)
|
||||
|
||||
|
||||
|
||||
def exchange_private_api_configured():
|
||||
return bool(OKX_API_KEY and OKX_API_SECRET and OKX_API_PASSPHRASE)
|
||||
|
||||
|
||||
def _position_row_effective_contracts(p):
|
||||
info = p.get("info", {}) or {}
|
||||
contracts = p.get("contracts")
|
||||
if contracts is None:
|
||||
raw_pos = info.get("pos")
|
||||
try:
|
||||
contracts = abs(float(raw_pos)) if raw_pos is not None else 0.0
|
||||
except Exception:
|
||||
contracts = 0.0
|
||||
try:
|
||||
return float(contracts)
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
|
||||
def _position_matches_wanted_contract(exchange_symbol, position):
|
||||
if not position:
|
||||
return False
|
||||
sym = position.get("symbol")
|
||||
return sym == exchange_symbol
|
||||
|
||||
|
||||
def _select_live_position_row(rows, exchange_symbol, direction, relax_hedge=False):
|
||||
if not rows:
|
||||
return None
|
||||
candidates = []
|
||||
for p in rows:
|
||||
if not _position_matches_wanted_contract(exchange_symbol, p):
|
||||
continue
|
||||
info = p.get("info", {}) or {}
|
||||
side = (p.get("side") or info.get("posSide") or "").lower()
|
||||
contracts = _position_row_effective_contracts(p)
|
||||
if contracts <= 0:
|
||||
continue
|
||||
if (not relax_hedge) and OKX_POS_MODE == "hedge":
|
||||
if side and side != (direction or "").lower():
|
||||
continue
|
||||
candidates.append((contracts, p))
|
||||
if not candidates and (not relax_hedge) and OKX_POS_MODE == "hedge":
|
||||
return _select_live_position_row(rows, exchange_symbol, direction, relax_hedge=True)
|
||||
if not candidates:
|
||||
return None
|
||||
candidates.sort(key=lambda x: x[0], reverse=True)
|
||||
return candidates[0][1]
|
||||
|
||||
|
||||
def parse_ccxt_position_metrics(position, order_leverage=None):
|
||||
if not position:
|
||||
return None
|
||||
p = position
|
||||
info = p.get("info", {}) or {}
|
||||
initial = _coerce_float(p.get("collateral"), p.get("initialMargin"), p.get("margin"))
|
||||
if initial is None or initial <= 0:
|
||||
initial = _coerce_float(
|
||||
info.get("margin"),
|
||||
info.get("imr"),
|
||||
info.get("initial_margin"),
|
||||
)
|
||||
notional = _coerce_float(p.get("notional"), p.get("notionalValue"))
|
||||
if notional is None or notional <= 0:
|
||||
notional = _coerce_float(info.get("notionalUsd"), info.get("notional"))
|
||||
if notional is not None:
|
||||
notional = abs(notional)
|
||||
if (initial is None or initial <= 0) and notional and notional > 0 and order_leverage:
|
||||
try:
|
||||
lev = float(order_leverage)
|
||||
if lev > 0:
|
||||
approx = notional / lev
|
||||
if approx > 0:
|
||||
initial = approx
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
unrealized = _coerce_float(
|
||||
p.get("unrealizedPnl"),
|
||||
info.get("upl"),
|
||||
info.get("unrealized_pnl"),
|
||||
)
|
||||
mark = _coerce_float(p.get("markPrice"), p.get("mark_price"), info.get("markPx"))
|
||||
out = {}
|
||||
if initial is not None and initial > 0:
|
||||
out["initial_margin"] = round(initial, FUNDS_DECIMALS)
|
||||
if notional is not None and notional > 0:
|
||||
out["notional"] = round(notional, FUNDS_DECIMALS)
|
||||
if unrealized is not None:
|
||||
out["unrealized_pnl"] = round(unrealized, FUNDS_DECIMALS)
|
||||
if mark is not None and mark > 0:
|
||||
out["mark_price"] = round(mark, 8)
|
||||
return out or None
|
||||
|
||||
|
||||
def _resolve_tpsl_prices_for_manual(direction, live_price, sltp_mode, data):
|
||||
sltp_mode = (sltp_mode or "price").strip().lower()
|
||||
if sltp_mode == "pct":
|
||||
sl_pct = float(data.get("sl_pct") or 0)
|
||||
tp_pct = float(data.get("tp_pct") or 0)
|
||||
if sl_pct <= 0 or tp_pct <= 0:
|
||||
raise ValueError("百分比止盈止损须为正数")
|
||||
sl_ratio = sl_pct / 100.0
|
||||
tp_ratio = tp_pct / 100.0
|
||||
entry = float(live_price)
|
||||
if direction == "short":
|
||||
stop_loss = entry * (1 + sl_ratio)
|
||||
take_profit = entry * (1 - tp_ratio)
|
||||
else:
|
||||
stop_loss = entry * (1 - sl_ratio)
|
||||
take_profit = entry * (1 + tp_ratio)
|
||||
else:
|
||||
stop_loss = float(data.get("sl") or data.get("stop_loss") or 0)
|
||||
take_profit = float(data.get("tp") or data.get("take_profit") or data.get("tgt") or 0)
|
||||
if stop_loss <= 0 or take_profit <= 0:
|
||||
raise ValueError("止盈止损价格须大于 0")
|
||||
return stop_loss, take_profit
|
||||
|
||||
|
||||
def _okx_tpsl_slot_from_order(order, exchange_symbol):
|
||||
info = order.get("info") or {}
|
||||
oid = order.get("id") or info.get("algoId") or info.get("ordId")
|
||||
trig = _coerce_float(
|
||||
info.get("slTriggerPx"),
|
||||
info.get("tpTriggerPx"),
|
||||
order.get("stopLossPrice"),
|
||||
order.get("takeProfitPrice"),
|
||||
)
|
||||
if trig is None:
|
||||
return None
|
||||
return {
|
||||
"order_id": str(oid) if oid is not None else None,
|
||||
"trigger_price": float(trig),
|
||||
"trigger_display": format_price_for_symbol(
|
||||
exchange_symbol.replace(":USDT", "").replace("/USDT:USDT", ""),
|
||||
trig,
|
||||
),
|
||||
"type": str(order.get("type") or info.get("ordType") or ""),
|
||||
}
|
||||
|
||||
|
||||
def fetch_exchange_tpsl_slots(exchange_symbol, direction, plan_sl=None, plan_tp=None):
|
||||
slots = {"sl": None, "tp": None}
|
||||
if not exchange_symbol:
|
||||
return slots
|
||||
ok, _ = ensure_okx_live_ready()
|
||||
if not ok:
|
||||
return slots
|
||||
try:
|
||||
ensure_markets_loaded()
|
||||
ambiguous = []
|
||||
for order in exchange.fetch_open_orders(exchange_symbol) or []:
|
||||
slot = _okx_tpsl_slot_from_order(order, exchange_symbol)
|
||||
if not slot or not slot.get("order_id"):
|
||||
continue
|
||||
trig = slot.get("trigger_price")
|
||||
if plan_sl is not None and plan_tp is not None:
|
||||
try:
|
||||
role = "sl" if abs(trig - float(plan_sl)) <= abs(trig - float(plan_tp)) else "tp"
|
||||
except Exception:
|
||||
role = None
|
||||
elif plan_sl is not None:
|
||||
role = "sl"
|
||||
elif plan_tp is not None:
|
||||
role = "tp"
|
||||
else:
|
||||
ambiguous.append(slot)
|
||||
continue
|
||||
if role in ("sl", "tp") and slots[role] is None:
|
||||
slots[role] = slot
|
||||
for slot in ambiguous:
|
||||
trig = slot.get("trigger_price")
|
||||
if trig is None:
|
||||
continue
|
||||
try:
|
||||
plan_sl_f = float(plan_sl) if plan_sl is not None else None
|
||||
plan_tp_f = float(plan_tp) if plan_tp is not None else None
|
||||
except Exception:
|
||||
plan_sl_f = plan_tp_f = None
|
||||
if plan_sl_f is not None and plan_tp_f is not None:
|
||||
role = "sl" if abs(trig - plan_sl_f) <= abs(trig - plan_tp_f) else "tp"
|
||||
elif plan_sl_f is not None:
|
||||
role = "sl"
|
||||
elif plan_tp_f is not None:
|
||||
role = "tp"
|
||||
else:
|
||||
continue
|
||||
if slots[role] is None:
|
||||
slots[role] = slot
|
||||
except Exception:
|
||||
pass
|
||||
return slots
|
||||
|
||||
|
||||
def cancel_okx_tpsl_slot(exchange_symbol, slot):
|
||||
if not slot or not exchange_symbol:
|
||||
return
|
||||
oid = slot.get("order_id")
|
||||
if not oid:
|
||||
return
|
||||
ensure_markets_loaded()
|
||||
exchange.cancel_order(str(oid), exchange_symbol)
|
||||
|
||||
|
||||
def replace_active_monitor_tpsl_on_exchange(order_row, stop_loss, take_profit):
|
||||
"""先撤该合约挂单/条件单,再按新价重挂 TP/SL。"""
|
||||
ok, reason = ensure_okx_live_ready()
|
||||
@@ -4146,8 +4381,8 @@ def render_main_page(page="trade"):
|
||||
session_row = ensure_session(conn, trading_day)
|
||||
local_current_capital = float(session_row["current_capital"])
|
||||
funding_capital, trading_capital = get_exchange_capitals()
|
||||
total_capital = round(funding_capital, 4) if funding_capital is not None else TOTAL_CAPITAL
|
||||
current_capital = round(trading_capital, 4) if trading_capital is not None else round(local_current_capital, 4)
|
||||
funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None
|
||||
current_capital = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else round(local_current_capital, FUNDS_DECIMALS)
|
||||
recommended_capital = get_recommended_capital(current_capital)
|
||||
key_list = conn.execute("SELECT * FROM key_monitors").fetchall()
|
||||
key_history = conn.execute(
|
||||
@@ -4176,11 +4411,13 @@ def render_main_page(page="trade"):
|
||||
)
|
||||
rate = round(win/total*100,2) if total else 0
|
||||
active_count = len(order_list)
|
||||
can_trade = now.hour >= TRADING_DAY_RESET_HOUR and active_count == 0
|
||||
can_trade = trading_day_reset_allows_new_open(now) and active_count < MAX_ACTIVE_POSITIONS
|
||||
key_gate_rule_text = (
|
||||
f"周期 {KLINE_TIMEFRAME}|量能/突破/二确门控见箱体与收敛规则|"
|
||||
f"周期 {KLINE_TIMEFRAME}|确认K:突破棒偏移 {KEY_CONFIRM_BREAKOUT_BAR}、确认棒偏移 {KEY_CONFIRM_BAR}|"
|
||||
f"量能:突破量 > 前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}|"
|
||||
f"自动开仓盈亏比 > {KEY_AUTO_MIN_PLANNED_RR}:1|日成交量排名前 {KEY_DAILY_VOLUME_RANK_MAX}|"
|
||||
f"斐波:添加后立即挂限价 @ E,失效按标记价触达 H/L(未成交撤单)"
|
||||
f"箱体/收敛可选 SL/TP 方案(标准 / 箱体1R·止盈1.5H / 趋势单+自填止盈)|移动保本默认关|"
|
||||
f"斐波:限价 @ E(SL/TP 为 H/L),可选移动保本|趋势止损外侧 {KEY_TREND_STOP_OUTSIDE_PCT}%"
|
||||
)
|
||||
strategy_extra = {}
|
||||
if page in ("strategy", "strategy_trend", "strategy_roll"):
|
||||
@@ -4206,7 +4443,6 @@ def render_main_page(page="trade"):
|
||||
miss_count=miss_count,
|
||||
rate=rate,
|
||||
trading_day=trading_day,
|
||||
total_capital=total_capital,
|
||||
daily_start_capital=DAILY_START_CAPITAL,
|
||||
current_capital=current_capital,
|
||||
recommended_capital=recommended_capital,
|
||||
@@ -4242,7 +4478,13 @@ def render_main_page(page="trade"):
|
||||
entry_reason_options=list(ENTRY_REASON_OPTIONS),
|
||||
entry_reason_other_value=ENTRY_REASON_OTHER,
|
||||
key_gate_rule_text=key_gate_rule_text,
|
||||
funds_fmt=format_funds_u,
|
||||
exchange_display=EXCHANGE_DISPLAY_NAME,
|
||||
max_active_positions=MAX_ACTIVE_POSITIONS,
|
||||
manual_min_planned_rr=MANUAL_MIN_PLANNED_RR,
|
||||
key_auto_min_planned_rr=KEY_AUTO_MIN_PLANNED_RR,
|
||||
kline_timeframe=KLINE_TIMEFRAME,
|
||||
funding_usdt=funding_usdt,
|
||||
**strategy_extra,
|
||||
)
|
||||
|
||||
@@ -4253,6 +4495,12 @@ def index():
|
||||
return redirect("/trade")
|
||||
|
||||
|
||||
@app.route("/key_monitor")
|
||||
@login_required
|
||||
def key_monitor_page():
|
||||
return render_main_page("key_monitor")
|
||||
|
||||
|
||||
@app.route("/trade")
|
||||
@login_required
|
||||
def trade_page():
|
||||
@@ -4280,21 +4528,23 @@ def api_account_snapshot():
|
||||
session_row = ensure_session(conn, trading_day)
|
||||
local_current_capital = float(session_row["current_capital"])
|
||||
funding_capital, trading_capital = get_exchange_capitals(force=True)
|
||||
total_capital = round(funding_capital, 4) if funding_capital is not None else TOTAL_CAPITAL
|
||||
current_capital = round(trading_capital, 4) if trading_capital is not None else round(local_current_capital, 4)
|
||||
funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None
|
||||
current_capital = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else round(local_current_capital, FUNDS_DECIMALS)
|
||||
recommended_capital = get_recommended_capital(current_capital)
|
||||
active_count = conn.execute("SELECT COUNT(*) FROM order_monitors WHERE status='active'").fetchone()[0]
|
||||
active_count = get_active_position_count(conn)
|
||||
conn.close()
|
||||
can_trade = now.hour >= TRADING_DAY_RESET_HOUR and active_count == 0
|
||||
can_trade = trading_day_reset_allows_new_open(now) and active_count < MAX_ACTIVE_POSITIONS
|
||||
available_trading_usdt = get_available_trading_usdt()
|
||||
return jsonify({
|
||||
"total_capital": total_capital,
|
||||
"funding_usdt": funding_usdt,
|
||||
"current_capital": current_capital,
|
||||
"available_trading_usdt": round(available_trading_usdt, 4) if available_trading_usdt is not None else None,
|
||||
"available_trading_usdt": round(available_trading_usdt, FUNDS_DECIMALS) if available_trading_usdt is not None else None,
|
||||
"recommended_capital": recommended_capital,
|
||||
"active_count": active_count,
|
||||
"max_active_positions": MAX_ACTIVE_POSITIONS,
|
||||
"can_trade": can_trade,
|
||||
"trading_day": trading_day
|
||||
"manual_min_planned_rr": MANUAL_MIN_PLANNED_RR,
|
||||
"trading_day": trading_day,
|
||||
})
|
||||
|
||||
|
||||
@@ -4306,10 +4556,15 @@ def api_price_snapshot():
|
||||
"SELECT id,symbol,monitor_type,direction,upper,lower,fib_entry_price,fib_limit_order_id FROM key_monitors"
|
||||
).fetchall()
|
||||
order_rows = conn.execute(
|
||||
"SELECT id,symbol,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage FROM order_monitors WHERE status='active'"
|
||||
"SELECT id,symbol,exchange_symbol,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage FROM order_monitors WHERE status='active'"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
|
||||
try:
|
||||
ensure_markets_loaded()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
symbol_set = set()
|
||||
for r in key_rows:
|
||||
symbol_set.add(r["symbol"])
|
||||
@@ -4322,20 +4577,30 @@ def api_price_snapshot():
|
||||
if p is not None:
|
||||
prices[s] = float(p)
|
||||
|
||||
all_swap_positions = []
|
||||
if exchange_private_api_configured():
|
||||
try:
|
||||
ensure_markets_loaded()
|
||||
# 显式 USDT 本位;不传 symbols 拉全量,再在本地按合约对齐
|
||||
all_swap_positions = exchange.fetch_positions(None, {"instType": OKX_POSITION_INST_TYPE}) or []
|
||||
except Exception:
|
||||
try:
|
||||
all_swap_positions = exchange.fetch_positions() or []
|
||||
except Exception:
|
||||
all_swap_positions = []
|
||||
|
||||
key_prices = []
|
||||
for r in key_rows:
|
||||
price = prices.get(r["symbol"])
|
||||
is_fib = is_fib_key_monitor_type(r["monitor_type"])
|
||||
if is_fib:
|
||||
price = get_symbol_mark_price(r["symbol"])
|
||||
else:
|
||||
price = prices.get(r["symbol"])
|
||||
if price is None:
|
||||
continue
|
||||
upper_diff, upper_pct = calc_price_diff_pct(price, r["upper"])
|
||||
lower_diff, lower_pct = calc_price_diff_pct(price, r["lower"])
|
||||
is_fib = is_fib_key_monitor_type(r["monitor_type"])
|
||||
gate = None
|
||||
if not is_fib:
|
||||
try:
|
||||
gate = _key_hard_checks(r["symbol"], (r["direction"] or "long").lower(), r["upper"], r["lower"], r["monitor_type"])
|
||||
except Exception:
|
||||
gate = None
|
||||
gate_summary = "-"
|
||||
gate_metrics = ""
|
||||
fib_gate_ok = True
|
||||
@@ -4344,11 +4609,16 @@ def api_price_snapshot():
|
||||
inval = fib_invalidate_by_mark(direction, price, r["upper"], r["lower"])
|
||||
fib_gate_ok = not inval
|
||||
entry = _sqlite_row_val(r, "fib_entry_price")
|
||||
entry_txt = format_price_for_symbol(r["symbol"], entry) if entry is not None else "-"
|
||||
entry_txt = format_price_for_symbol(r["symbol"], entry) if entry else "-"
|
||||
gate_summary = f"斐波 挂E={entry_txt} {'标记价将失效' if inval else '等待成交'}"
|
||||
if _sqlite_row_val(r, "fib_limit_order_id"):
|
||||
gate_metrics = f"限价单:{_sqlite_row_val(r, 'fib_limit_order_id')}"
|
||||
elif gate:
|
||||
else:
|
||||
try:
|
||||
gate = _key_hard_checks(r["symbol"], (r["direction"] or "long").lower(), r["upper"], r["lower"], r["monitor_type"])
|
||||
except Exception:
|
||||
gate = None
|
||||
if gate:
|
||||
rank_seg = "ERR" if int(gate.get("rank_total") or 0) <= 0 else f"{gate.get('rank')}/{gate.get('rank_total')}"
|
||||
gate_summary = (
|
||||
f"量:{'Y' if gate.get('vol_ok') else 'N'} "
|
||||
@@ -4371,10 +4641,16 @@ def api_price_snapshot():
|
||||
)
|
||||
except Exception:
|
||||
gate_metrics = ""
|
||||
px_disp = format_price_for_symbol(r["symbol"], price)
|
||||
try:
|
||||
price_num = float(px_disp) if px_disp != "-" else float(price)
|
||||
except Exception:
|
||||
price_num = float(price)
|
||||
key_prices.append({
|
||||
"id": r["id"],
|
||||
"symbol": r["symbol"],
|
||||
"price": round(price, 6),
|
||||
"price": price_num,
|
||||
"price_display": px_disp,
|
||||
"upper_diff": upper_diff,
|
||||
"upper_pct": upper_pct,
|
||||
"lower_diff": lower_diff,
|
||||
@@ -4395,19 +4671,67 @@ def api_price_snapshot():
|
||||
pnl = calc_pnl(r["direction"], entry, price, margin, leverage) if entry > 0 else 0
|
||||
pnl_pct = round((pnl / margin * 100), 4) if margin > 0 else 0
|
||||
rr_ratio = calc_rr_ratio(r["direction"], entry, r["initial_stop_loss"] or r["stop_loss"], r["take_profit"])
|
||||
order_prices.append({
|
||||
ex_sym = resolve_monitor_exchange_symbol(r)
|
||||
prow = _select_live_position_row(all_swap_positions, ex_sym, r["direction"])
|
||||
lev_row = r["leverage"] if "leverage" in r.keys() else None
|
||||
ex_metrics = parse_ccxt_position_metrics(prow, order_leverage=lev_row) if prow else None
|
||||
payload = {
|
||||
"id": r["id"],
|
||||
"symbol": r["symbol"],
|
||||
"price": round(price, 6),
|
||||
"float_pnl": round(pnl, 6),
|
||||
"float_pnl": round(pnl, 2),
|
||||
"float_pct": pnl_pct,
|
||||
"rr_ratio": rr_ratio,
|
||||
})
|
||||
"plan_margin": round(margin, 2) if margin else None,
|
||||
"exchange_initial_margin": None,
|
||||
"exchange_notional": None,
|
||||
"exchange_mark_price": None,
|
||||
"pnl_source": "plan",
|
||||
}
|
||||
if ex_metrics:
|
||||
if ex_metrics.get("initial_margin") is not None:
|
||||
payload["exchange_initial_margin"] = ex_metrics["initial_margin"]
|
||||
if ex_metrics.get("notional") is not None:
|
||||
payload["exchange_notional"] = ex_metrics["notional"]
|
||||
if ex_metrics.get("mark_price") is not None:
|
||||
payload["exchange_mark_price"] = ex_metrics["mark_price"]
|
||||
if ex_metrics.get("unrealized_pnl") is not None:
|
||||
payload["float_pnl"] = round(float(ex_metrics["unrealized_pnl"]), 2)
|
||||
payload["pnl_source"] = "exchange"
|
||||
denom = ex_metrics.get("initial_margin") or margin
|
||||
payload["float_pct"] = (
|
||||
round((payload["float_pnl"] / float(denom)) * 100, 4) if denom and float(denom) > 0 else pnl_pct
|
||||
)
|
||||
px_for_fmt = float(price)
|
||||
if ex_metrics and ex_metrics.get("mark_price") is not None:
|
||||
try:
|
||||
px_for_fmt = float(ex_metrics["mark_price"])
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
px_disp = format_price_for_symbol(r["symbol"], px_for_fmt)
|
||||
try:
|
||||
payload["price"] = float(px_disp) if px_disp != "-" else px_for_fmt
|
||||
except Exception:
|
||||
payload["price"] = px_for_fmt
|
||||
payload["price_display"] = px_disp
|
||||
if exchange_private_api_configured():
|
||||
try:
|
||||
payload["exchange_tpsl"] = fetch_exchange_tpsl_slots(
|
||||
ex_sym,
|
||||
r["direction"],
|
||||
plan_sl=r["stop_loss"],
|
||||
plan_tp=r["take_profit"],
|
||||
)
|
||||
except Exception:
|
||||
payload["exchange_tpsl"] = {"sl": None, "tp": None}
|
||||
else:
|
||||
payload["exchange_tpsl"] = {"sl": None, "tp": None}
|
||||
order_prices.append(payload)
|
||||
|
||||
return jsonify({
|
||||
"updated_at": app_now_str(),
|
||||
"key_prices": key_prices,
|
||||
"order_prices": order_prices
|
||||
"order_prices": order_prices,
|
||||
"positions_raw_count": len(all_swap_positions),
|
||||
})
|
||||
|
||||
|
||||
@@ -4669,6 +4993,94 @@ def api_key_kline():
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/order/<int:order_id>/cancel_tpsl", methods=["POST"])
|
||||
@login_required
|
||||
def api_order_cancel_tpsl(order_id):
|
||||
data = request.get_json(silent=True) or {}
|
||||
role = (data.get("role") or "").strip().lower()
|
||||
if role not in ("sl", "tp"):
|
||||
return jsonify({"ok": False, "msg": "role 须为 sl 或 tp"}), 400
|
||||
conn = get_db()
|
||||
row = conn.execute(
|
||||
"SELECT * FROM order_monitors WHERE id=? AND status='active'",
|
||||
(order_id,),
|
||||
).fetchone()
|
||||
conn.close()
|
||||
if not row:
|
||||
return jsonify({"ok": False, "msg": "持仓不存在或已结束"}), 404
|
||||
ok, reason = ensure_okx_live_ready()
|
||||
if not ok:
|
||||
return jsonify({"ok": False, "msg": reason}), 400
|
||||
ex_sym = resolve_monitor_exchange_symbol(row)
|
||||
slots = fetch_exchange_tpsl_slots(ex_sym, row["direction"], plan_sl=row["stop_loss"], plan_tp=row["take_profit"])
|
||||
slot = slots.get(role)
|
||||
if not slot:
|
||||
return jsonify({"ok": False, "msg": f"交易所未找到{'止损' if role == 'sl' else '止盈'}委托"}), 404
|
||||
try:
|
||||
cancel_okx_tpsl_slot(ex_sym, slot)
|
||||
return jsonify({"ok": True, "msg": "已撤单", "exchange_tpsl": fetch_exchange_tpsl_slots(ex_sym, row["direction"], plan_sl=row["stop_loss"], plan_tp=row["take_profit"])})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": friendly_exchange_error(e)}), 400
|
||||
|
||||
|
||||
@app.route("/api/order/<int:order_id>/place_tpsl", methods=["POST"])
|
||||
@login_required
|
||||
def api_order_place_tpsl(order_id):
|
||||
data = request.get_json(silent=True) or {}
|
||||
conn = get_db()
|
||||
row = conn.execute(
|
||||
"SELECT * FROM order_monitors WHERE id=? AND status='active'",
|
||||
(order_id,),
|
||||
).fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "msg": "持仓不存在或已结束"}), 404
|
||||
symbol = row["symbol"]
|
||||
direction = row["direction"]
|
||||
live_price = get_price(symbol)
|
||||
if live_price is None:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "msg": "获取交易所实时价格失败"}), 400
|
||||
try:
|
||||
sltp_mode = (data.get("sltp_mode") or "price").strip().lower()
|
||||
stop_loss, take_profit = _resolve_tpsl_prices_for_manual(direction, live_price, sltp_mode, data)
|
||||
except Exception as e:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "msg": str(e)}), 400
|
||||
planned_rr = calc_rr_ratio(direction, live_price, stop_loss, take_profit)
|
||||
if planned_rr is None or planned_rr < MANUAL_MIN_PLANNED_RR:
|
||||
conn.close()
|
||||
rr_txt = f"{planned_rr:.4f}" if planned_rr is not None else "无法计算"
|
||||
return jsonify(
|
||||
{
|
||||
"ok": False,
|
||||
"msg": f"计划盈亏比 {rr_txt}:1 低于最低要求 {MANUAL_MIN_PLANNED_RR}:1",
|
||||
}
|
||||
), 400
|
||||
try:
|
||||
replace_active_monitor_tpsl_on_exchange(row, stop_loss, take_profit)
|
||||
except Exception as e:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "msg": friendly_exchange_error(e)}), 400
|
||||
conn.execute(
|
||||
"UPDATE order_monitors SET stop_loss=?, take_profit=? WHERE id=?",
|
||||
(stop_loss, take_profit, order_id),
|
||||
)
|
||||
conn.commit()
|
||||
ex_sym = resolve_monitor_exchange_symbol(row)
|
||||
slots = fetch_exchange_tpsl_slots(ex_sym, direction, plan_sl=stop_loss, plan_tp=take_profit)
|
||||
conn.close()
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
"msg": "已先撤后挂止盈止损",
|
||||
"stop_loss": stop_loss,
|
||||
"take_profit": take_profit,
|
||||
"planned_rr": planned_rr,
|
||||
"exchange_tpsl": slots,
|
||||
}
|
||||
)
|
||||
|
||||
@app.route("/add_key", methods=["POST"])
|
||||
@login_required
|
||||
def add_key():
|
||||
@@ -4676,11 +5088,11 @@ def add_key():
|
||||
symbol = normalize_symbol_input(d.get("symbol"))
|
||||
if not symbol:
|
||||
flash("symbol 不能为空")
|
||||
return redirect("/")
|
||||
return redirect("/key_monitor")
|
||||
direction_sel = (d.get("direction") or "").strip().lower()
|
||||
if direction_sel not in ("long", "short"):
|
||||
flash("请选择做多或做空")
|
||||
return redirect("/")
|
||||
return redirect("/key_monitor")
|
||||
mt = (d.get("type") or "").strip()
|
||||
allowed_types = tuple(KEY_MONITOR_AUTO_TYPES) + tuple(KEY_MONITOR_ALERT_ONLY_TYPES) + tuple(FIB_KEY_MONITOR_TYPES)
|
||||
if mt not in allowed_types:
|
||||
@@ -4695,9 +5107,13 @@ def add_key():
|
||||
return redirect("/")
|
||||
conn = get_db()
|
||||
if mt in KEY_MONITOR_AUTO_TYPES:
|
||||
if get_active_position_count(conn) > 0:
|
||||
occupied = get_active_position_count(conn)
|
||||
if occupied >= MAX_ACTIVE_POSITIONS:
|
||||
conn.close()
|
||||
flash("当前已有持仓:无法添加「箱体突破 / 收敛突破」(请先平仓或使用阻力/支撑/斐波类型)")
|
||||
flash(
|
||||
f"当前持仓已达上限({occupied}/{MAX_ACTIVE_POSITIONS}):无法添加「箱体突破 / 收敛突破」。"
|
||||
"请先平仓或使用阻力/支撑/斐波类型"
|
||||
)
|
||||
return redirect("/")
|
||||
ex_sym_key = normalize_okx_symbol(symbol)
|
||||
try:
|
||||
@@ -4759,7 +5175,7 @@ def add_key():
|
||||
if mt in KEY_MONITOR_AUTO_TYPES:
|
||||
extra = f"|方案:{sl_tp_mode_label(sl_tp_mode)}|移动保本:{'开' if be_flag else '关'}"
|
||||
flash(f"添加成功({symbol} 日成交量排名 {rank}/{total}){extra}")
|
||||
return redirect("/")
|
||||
return redirect("/key_monitor")
|
||||
|
||||
@app.route("/add_order", methods=["POST"])
|
||||
@login_required
|
||||
@@ -4772,10 +5188,10 @@ def add_order():
|
||||
if not symbol:
|
||||
conn.close()
|
||||
flash("symbol 不能为空")
|
||||
return redirect("/")
|
||||
return redirect("/trade")
|
||||
ok, reason = precheck_risk(conn, symbol, direction)
|
||||
if not ok:
|
||||
if "一次只能持有一个仓位" in reason:
|
||||
if "已达最大持仓数" in reason or "一次只能持有一个仓位" in reason:
|
||||
try:
|
||||
tp_raw = parse_positive_float(d.get("tp"))
|
||||
sl_raw = parse_positive_float(d.get("sl"))
|
||||
@@ -4798,12 +5214,12 @@ def add_order():
|
||||
conn.commit()
|
||||
conn.close()
|
||||
flash(f"风控拒绝下单:{reason}")
|
||||
return redirect("/")
|
||||
return redirect("/trade")
|
||||
ok_live, reason_live = ensure_okx_live_ready()
|
||||
if not ok_live:
|
||||
conn.close()
|
||||
flash(f"风控拒绝下单:{reason_live}")
|
||||
return redirect("/")
|
||||
return redirect("/trade")
|
||||
exchange_symbol = normalize_okx_symbol(symbol)
|
||||
default_leverage = get_synced_leverage(exchange_symbol, direction) or infer_leverage(symbol)
|
||||
try:
|
||||
@@ -4812,11 +5228,11 @@ def add_order():
|
||||
except Exception:
|
||||
conn.close()
|
||||
flash("杠杆参数格式错误")
|
||||
return redirect("/")
|
||||
return redirect("/trade")
|
||||
if leverage <= 0:
|
||||
conn.close()
|
||||
flash("杠杆必须大于0")
|
||||
return redirect("/")
|
||||
return redirect("/trade")
|
||||
|
||||
trading_day = get_trading_day(now)
|
||||
opens_today_before = conn.execute(
|
||||
@@ -4834,7 +5250,7 @@ def add_order():
|
||||
if live_price is None:
|
||||
conn.close()
|
||||
flash("获取交易所实时价格失败,请稍后重试")
|
||||
return redirect("/")
|
||||
return redirect("/trade")
|
||||
sltp_mode = (d.get("sltp_mode") or "price").strip().lower()
|
||||
if sltp_mode not in ("price", "pct"):
|
||||
sltp_mode = "price"
|
||||
@@ -4855,7 +5271,7 @@ def add_order():
|
||||
except Exception:
|
||||
conn.close()
|
||||
flash("百分比止盈止损参数错误,请填写正数百分比")
|
||||
return redirect("/")
|
||||
return redirect("/trade")
|
||||
else:
|
||||
try:
|
||||
stop_loss = float(d["sl"])
|
||||
@@ -4863,16 +5279,22 @@ def add_order():
|
||||
except Exception:
|
||||
conn.close()
|
||||
flash("价格参数格式错误")
|
||||
return redirect("/")
|
||||
return redirect("/trade")
|
||||
if stop_loss <= 0 or take_profit <= 0:
|
||||
conn.close()
|
||||
flash("价格参数必须大于0")
|
||||
return redirect("/")
|
||||
return redirect("/trade")
|
||||
planned_rr_manual = calc_rr_ratio(direction, live_price, stop_loss, take_profit)
|
||||
if planned_rr_manual is None or planned_rr_manual < MANUAL_MIN_PLANNED_RR:
|
||||
conn.close()
|
||||
rr_txt = f"{planned_rr_manual:.4f}" if planned_rr_manual is not None else "无法计算"
|
||||
flash(f"风控拒绝下单:计划盈亏比 {rr_txt}:1 低于最低要求 {MANUAL_MIN_PLANNED_RR}:1")
|
||||
return redirect("/trade")
|
||||
risk_fraction = calc_risk_fraction(direction, live_price, stop_loss)
|
||||
if risk_fraction is None:
|
||||
conn.close()
|
||||
flash("止损方向不合法:请检查入场方向与止损价格关系")
|
||||
return redirect("/")
|
||||
return redirect("/trade")
|
||||
risk_percent = max(0.01, float(RISK_PERCENT))
|
||||
risk_amount = round(capital_base * risk_percent / 100.0, 4)
|
||||
notional_value = round(risk_amount / risk_fraction, 4)
|
||||
@@ -4880,13 +5302,13 @@ def add_order():
|
||||
if capital_base and margin_capital > capital_base:
|
||||
conn.close()
|
||||
flash("以损定仓后保证金超过当前交易资金,请放宽止损或降低风险比例")
|
||||
return redirect("/")
|
||||
return redirect("/trade")
|
||||
if available_usdt is not None:
|
||||
max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), 4)
|
||||
if margin_capital > max_margin:
|
||||
conn.close()
|
||||
flash(f"保证金不足:交易账户可用约 {round(available_usdt,4)}U,当前最多建议 {max_margin}U")
|
||||
return redirect("/")
|
||||
return redirect("/trade")
|
||||
position_ratio = round(margin_capital / capital_base * 100, 2) if capital_base else 0
|
||||
try:
|
||||
amount, quote_price = prepare_order_amount(exchange_symbol, margin_capital, leverage, live_price)
|
||||
@@ -4899,7 +5321,7 @@ def add_order():
|
||||
except Exception as e:
|
||||
conn.close()
|
||||
flash(friendly_okx_error(e, available_usdt=available_usdt))
|
||||
return redirect("/")
|
||||
return redirect("/trade")
|
||||
|
||||
make_order_chart = d.get("order_chart", "").lower() in ("1", "true", "on", "yes")
|
||||
opened_at_bj = app_now_str()
|
||||
@@ -5060,7 +5482,7 @@ def add_order():
|
||||
if advice:
|
||||
send_wechat_msg(f"【AI提醒】今日开仓次数已达 {opens_today_after}\n{advice[:800]}")
|
||||
flash(f"【AI提醒】今日开仓次数已达 {opens_today_after}:{advice[:300]}")
|
||||
return redirect("/")
|
||||
return redirect("/trade")
|
||||
|
||||
@app.route("/delete_key_monitor/<int:kid>", methods=["POST"])
|
||||
@login_required
|
||||
@@ -5920,10 +6342,11 @@ def _hub_meta_bundle():
|
||||
"key_gate_rule_text": (
|
||||
f"周期 {KLINE_TIMEFRAME}|量能/突破/二确门控见箱体与收敛规则|"
|
||||
f"自动开仓盈亏比 > {KEY_AUTO_MIN_PLANNED_RR}:1|日成交量排名前 {KEY_DAILY_VOLUME_RANK_MAX}|"
|
||||
f"斐波:添加后立即挂限价 @ E,失效按标记价触达 H/L(未成交撤单)"
|
||||
f"箱体/收敛可选 SL/TP 方案(标准 / 箱体1R·止盈1.5H / 趋势单+自填止盈)|移动保本默认关|"
|
||||
f"斐波:限价 @ E(SL/TP 为 H/L),可选移动保本|趋势止损外侧 {KEY_TREND_STOP_OUTSIDE_PCT}%"
|
||||
),
|
||||
"manual_min_planned_rr": float(os.getenv("MANUAL_MIN_PLANNED_RR", "1.4")),
|
||||
"max_active_positions": max(1, int(os.getenv("MAX_ACTIVE_POSITIONS", "1"))),
|
||||
"manual_min_planned_rr": MANUAL_MIN_PLANNED_RR,
|
||||
"max_active_positions": MAX_ACTIVE_POSITIONS,
|
||||
"btc_leverage": BTC_LEVERAGE,
|
||||
"alt_leverage": ALT_LEVERAGE,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user