你的说明
This commit is contained in:
@@ -0,0 +1,566 @@
|
||||
#!/usr/bin/env python3
|
||||
"""One-shot: align crypto_monitor_okx with binance/gate patterns (OKX_* prefixes)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OKX = ROOT / "crypto_monitor_okx"
|
||||
BIN = ROOT / "crypto_monitor_binance"
|
||||
GATE = ROOT / "crypto_monitor_gate"
|
||||
|
||||
|
||||
def patch_app():
|
||||
app_path = OKX / "app.py"
|
||||
text = app_path.read_text(encoding="utf-8")
|
||||
|
||||
if "EXCHANGE_DISPLAY_NAME" not in text.split("OKX_POS_MODE")[0]:
|
||||
text = text.replace(
|
||||
'OKX_POS_MODE = os.getenv("OKX_POS_MODE", "hedge")\n',
|
||||
'OKX_POS_MODE = os.getenv("OKX_POS_MODE", "hedge")\n'
|
||||
'EXCHANGE_DISPLAY_NAME = (os.getenv("EXCHANGE_DISPLAY_NAME") or "OKX").strip() or "OKX"\n',
|
||||
)
|
||||
|
||||
if "TRADING_DAY_RESET_OPEN_GUARD_ENABLED" not in text:
|
||||
text = text.replace(
|
||||
"TRADING_DAY_RESET_HOUR = int(os.getenv(\"TRADING_DAY_RESET_HOUR\", \"8\"))\nAPP_TIMEZONE",
|
||||
'TRADING_DAY_RESET_HOUR = int(os.getenv("TRADING_DAY_RESET_HOUR", "8"))\n'
|
||||
"TRADING_DAY_RESET_OPEN_GUARD_ENABLED = os.getenv(\n"
|
||||
' "TRADING_DAY_RESET_OPEN_GUARD_ENABLED", "true"\n'
|
||||
').lower() in ("1", "true", "yes", "on")\n'
|
||||
"APP_TIMEZONE",
|
||||
)
|
||||
|
||||
extra_env = """
|
||||
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"))
|
||||
"""
|
||||
if "MANUAL_MIN_PLANNED_RR = float" not in text:
|
||||
text = text.replace(
|
||||
"KEY_DAILY_VOLUME_RANK_MAX = int(os.getenv(\"KEY_DAILY_VOLUME_RANK_MAX\", \"30\"))\n",
|
||||
"KEY_DAILY_VOLUME_RANK_MAX = max(1, int(os.getenv(\"KEY_DAILY_VOLUME_RANK_MAX\", \"30\")))\n"
|
||||
+ extra_env,
|
||||
)
|
||||
|
||||
if "def format_funds_u" not in text:
|
||||
text = text.replace(
|
||||
"def format_hold_minutes(minutes):",
|
||||
'''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 "def trading_day_reset_allows_new_open" not in text:
|
||||
text = text.replace(
|
||||
"def precheck_risk(conn, symbol, direction):",
|
||||
'''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):''',
|
||||
)
|
||||
|
||||
text = re.sub(
|
||||
r"def precheck_risk\(conn, symbol, direction\):.*?return True, \"\"",
|
||||
'''def precheck_risk(conn, symbol, direction):
|
||||
now = app_now()
|
||||
if not trading_day_reset_allows_new_open(now):
|
||||
return False, f"北京时间 {TRADING_DAY_RESET_HOUR}:00 前不允许持仓"
|
||||
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"):
|
||||
expected = BTC_LEVERAGE
|
||||
else:
|
||||
expected = ALT_LEVERAGE
|
||||
if expected <= 0:
|
||||
return False, "杠杆配置异常"
|
||||
return True, ""''',
|
||||
text,
|
||||
count=1,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# _key_hard_checks from gate
|
||||
gate_text = (GATE / "app.py").read_text(encoding="utf-8")
|
||||
m = re.search(r"def _key_hard_checks\(symbol.*?return out\n", gate_text, re.DOTALL)
|
||||
if m:
|
||||
kh = m.group(0).replace("normalize_exchange_symbol", "normalize_okx_symbol")
|
||||
text = re.sub(r"def _key_hard_checks\(symbol.*?return out\n", kh, text, count=1, flags=re.DOTALL)
|
||||
|
||||
if "def exchange_private_api_configured" not in text:
|
||||
insert = '''
|
||||
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)
|
||||
|
||||
|
||||
'''
|
||||
text = text.replace(
|
||||
"def replace_active_monitor_tpsl_on_exchange(order_row, stop_loss, take_profit):",
|
||||
insert + "def replace_active_monitor_tpsl_on_exchange(order_row, stop_loss, take_profit):",
|
||||
)
|
||||
|
||||
# render_main_page funding + template vars (gate style)
|
||||
text = text.replace(
|
||||
" funding_capital, trading_capital = get_exchange_capitals()\n"
|
||||
" total_capital = round(funding_capital, 4) if funding_capital is not None else TOTAL_CAPITAL\n"
|
||||
" current_capital = round(trading_capital, 4) if trading_capital is not None else round(local_current_capital, 4)\n",
|
||||
" funding_capital, trading_capital = get_exchange_capitals()\n"
|
||||
" funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None\n"
|
||||
" current_capital = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else round(local_current_capital, FUNDS_DECIMALS)\n",
|
||||
)
|
||||
text = text.replace(
|
||||
" can_trade = now.hour >= TRADING_DAY_RESET_HOUR and active_count == 0\n"
|
||||
" key_gate_rule_text = (\n"
|
||||
' f"周期 {KLINE_TIMEFRAME}|量能/突破/二确门控见箱体与收敛规则|"\n',
|
||||
" can_trade = trading_day_reset_allows_new_open(now) and active_count < MAX_ACTIVE_POSITIONS\n"
|
||||
" key_gate_rule_text = (\n"
|
||||
' f"周期 {KLINE_TIMEFRAME}|确认K:突破棒偏移 {KEY_CONFIRM_BREAKOUT_BAR}、确认棒偏移 {KEY_CONFIRM_BAR}|"\n'
|
||||
' f"量能:突破量 > 前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}|"\n',
|
||||
)
|
||||
text = text.replace(
|
||||
' f"斐波:添加后立即挂限价 @ E,失效按标记价触达 H/L(未成交撤单)"\n',
|
||||
' f"箱体/收敛可选 SL/TP 方案(标准 / 箱体1R·止盈1.5H / 趋势单+自填止盈)|移动保本默认关|"\n'
|
||||
' f"斐波:限价 @ E(SL/TP 为 H/L),可选移动保本|趋势止损外侧 {KEY_TREND_STOP_OUTSIDE_PCT}%"\n',
|
||||
)
|
||||
text = text.replace(" total_capital=total_capital,\n", "")
|
||||
text = text.replace(
|
||||
" key_auto_min_planned_rr=KEY_AUTO_MIN_PLANNED_RR,\n **strategy_extra,",
|
||||
" funds_fmt=format_funds_u,\n"
|
||||
" exchange_display=EXCHANGE_DISPLAY_NAME,\n"
|
||||
" max_active_positions=MAX_ACTIVE_POSITIONS,\n"
|
||||
" manual_min_planned_rr=MANUAL_MIN_PLANNED_RR,\n"
|
||||
" key_auto_min_planned_rr=KEY_AUTO_MIN_PLANNED_RR,\n"
|
||||
" kline_timeframe=KLINE_TIMEFRAME,\n"
|
||||
" funding_usdt=funding_usdt,\n"
|
||||
" **strategy_extra,",
|
||||
)
|
||||
|
||||
if '@app.route("/key_monitor")' not in text:
|
||||
text = text.replace(
|
||||
'@app.route("/trade")\n@login_required\ndef trade_page():',
|
||||
'@app.route("/key_monitor")\n@login_required\ndef key_monitor_page():\n'
|
||||
' return render_main_page("key_monitor")\n\n\n'
|
||||
'@app.route("/trade")\n@login_required\ndef trade_page():',
|
||||
)
|
||||
|
||||
# account_snapshot
|
||||
text = re.sub(
|
||||
r"@app\.route\(\"/api/account_snapshot\"\).*?return jsonify\(\{[^}]+\}\)",
|
||||
'''@app.route("/api/account_snapshot")
|
||||
@login_required
|
||||
def api_account_snapshot():
|
||||
now = app_now()
|
||||
trading_day = get_trading_day(now)
|
||||
conn = get_db()
|
||||
session_row = ensure_session(conn, trading_day)
|
||||
local_current_capital = float(session_row["current_capital"])
|
||||
funding_capital, trading_capital = get_exchange_capitals(force=True)
|
||||
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 = get_active_position_count(conn)
|
||||
conn.close()
|
||||
can_trade = trading_day_reset_allows_new_open(now) and active_count < MAX_ACTIVE_POSITIONS
|
||||
available_trading_usdt = get_available_trading_usdt()
|
||||
return jsonify({
|
||||
"funding_usdt": funding_usdt,
|
||||
"current_capital": current_capital,
|
||||
"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,
|
||||
"manual_min_planned_rr": MANUAL_MIN_PLANNED_RR,
|
||||
"trading_day": trading_day,
|
||||
})''',
|
||||
text,
|
||||
count=1,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# api_price_snapshot from gate (OKX positions)
|
||||
gate_ps = re.search(
|
||||
r'@app\.route\("/api/price_snapshot"\).*?return jsonify\(\{[^}]+\}\)',
|
||||
gate_text,
|
||||
re.DOTALL,
|
||||
)
|
||||
if gate_ps:
|
||||
ps = gate_ps.group(0)
|
||||
ps = ps.replace("exchange_private_api_configured()", "exchange_private_api_configured()")
|
||||
ps = ps.replace(
|
||||
'all_swap_positions = exchange.fetch_positions(None, {"settle": "usdt"}) or []',
|
||||
'all_swap_positions = exchange.fetch_positions(None, {"instType": OKX_POSITION_INST_TYPE}) or []',
|
||||
)
|
||||
ps = ps.replace("fetch_exchange_tpsl_slots(", "fetch_exchange_tpsl_slots(")
|
||||
ps = ps.replace("cancel_gate_tpsl_slot", "cancel_okx_tpsl_slot")
|
||||
ps = ps.replace("ensure_exchange_live_ready", "ensure_okx_live_ready")
|
||||
text = re.sub(
|
||||
r'@app\.route\("/api/price_snapshot"\).*?return jsonify\(\{[^}]+\}\)',
|
||||
ps,
|
||||
text,
|
||||
count=1,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# cancel/place tpsl routes
|
||||
if 'api_order_cancel_tpsl' not in text:
|
||||
bin_text = (BIN / "app.py").read_text(encoding="utf-8")
|
||||
m = re.search(
|
||||
r'@app\.route\("/api/order/<int:order_id>/cancel_tpsl".*?exchange_tpsl": slots,\s*\}\s*\)',
|
||||
bin_text,
|
||||
re.DOTALL,
|
||||
)
|
||||
if m:
|
||||
block = m.group(0)
|
||||
block = block.replace("ensure_exchange_live_ready", "ensure_okx_live_ready")
|
||||
block = block.replace("cancel_binance_tpsl_slot", "cancel_okx_tpsl_slot")
|
||||
block = block.replace(
|
||||
'fetch_exchange_tpsl_slots(ex_sym, row["direction"])',
|
||||
'fetch_exchange_tpsl_slots(ex_sym, row["direction"], plan_sl=row["stop_loss"], plan_tp=row["take_profit"])',
|
||||
)
|
||||
block = block.replace(
|
||||
'fetch_exchange_tpsl_slots(ex_sym, direction)',
|
||||
'fetch_exchange_tpsl_slots(ex_sym, direction, plan_sl=stop_loss, plan_tp=take_profit)',
|
||||
)
|
||||
text = text.replace(
|
||||
'@app.route("/add_key", methods=["POST"])',
|
||||
block + '\n\n@app.route("/add_key", methods=["POST"])',
|
||||
)
|
||||
|
||||
# add_order RR + redirects
|
||||
if "planned_rr_manual" not in text:
|
||||
text = text.replace(
|
||||
" if stop_loss <= 0 or take_profit <= 0:\n"
|
||||
" conn.close()\n"
|
||||
" flash(\"价格参数必须大于0\")\n"
|
||||
" return redirect(\"/\")\n"
|
||||
" risk_fraction = calc_risk_fraction",
|
||||
" if stop_loss <= 0 or take_profit <= 0:\n"
|
||||
" conn.close()\n"
|
||||
" flash(\"价格参数必须大于0\")\n"
|
||||
" return redirect(\"/trade\")\n"
|
||||
" planned_rr_manual = calc_rr_ratio(direction, live_price, stop_loss, take_profit)\n"
|
||||
" if planned_rr_manual is None or planned_rr_manual < MANUAL_MIN_PLANNED_RR:\n"
|
||||
" conn.close()\n"
|
||||
" rr_txt = f\"{planned_rr_manual:.4f}\" if planned_rr_manual is not None else \"无法计算\"\n"
|
||||
" flash(f\"风控拒绝下单:计划盈亏比 {rr_txt}:1 低于最低要求 {MANUAL_MIN_PLANNED_RR}:1\")\n"
|
||||
" return redirect(\"/trade\")\n"
|
||||
" risk_fraction = calc_risk_fraction",
|
||||
)
|
||||
|
||||
text = text.replace(
|
||||
'if get_active_position_count(conn) > 0:\n'
|
||||
' conn.close()\n'
|
||||
' flash("当前已有持仓:无法添加「箱体突破 / 收敛突破」(请先平仓或使用阻力/支撑/斐波类型)")',
|
||||
'occupied = get_active_position_count(conn)\n'
|
||||
' if occupied >= MAX_ACTIVE_POSITIONS:\n'
|
||||
' conn.close()\n'
|
||||
' flash(\n'
|
||||
' f"当前持仓已达上限({occupied}/{MAX_ACTIVE_POSITIONS}):无法添加「箱体突破 / 收敛突破」。"\n'
|
||||
' "请先平仓或使用阻力/支撑/斐波类型"\n'
|
||||
' )',
|
||||
)
|
||||
|
||||
# add_key → /key_monitor (success paths in add_key only)
|
||||
text = text.replace(
|
||||
'def add_key():\n d = request.form\n symbol = normalize_symbol_input(d.get("symbol"))\n if not symbol:\n flash("symbol 不能为空")\n return redirect("/")',
|
||||
'def add_key():\n d = request.form\n symbol = normalize_symbol_input(d.get("symbol"))\n if not symbol:\n flash("symbol 不能为空")\n return redirect("/key_monitor")',
|
||||
)
|
||||
text = re.sub(
|
||||
r'(def add_key\(\):.*?)(return redirect\("/"\))',
|
||||
lambda m: m.group(1) + 'return redirect("/key_monitor")',
|
||||
text,
|
||||
count=0,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
text = text.replace(
|
||||
'if "一次只能持有一个仓位" in reason:',
|
||||
'if "已达最大持仓数" in reason or "一次只能持有一个仓位" in reason:',
|
||||
)
|
||||
|
||||
app_path.write_text(text, encoding="utf-8")
|
||||
print("patched", app_path)
|
||||
|
||||
|
||||
def copy_templates():
|
||||
src = BIN / "templates" / "index.html"
|
||||
dst = OKX / "templates" / "index.html"
|
||||
shutil.copy2(src, dst)
|
||||
print("copied", dst)
|
||||
|
||||
|
||||
def copy_env_example():
|
||||
bin_env = (BIN / ".env.example").read_text(encoding="utf-8")
|
||||
okx_path = OKX / ".env.example"
|
||||
okx = okx_path.read_text(encoding="utf-8")
|
||||
# inject binance-style blocks if missing
|
||||
for marker, block in [
|
||||
(
|
||||
"TRADING_DAY_RESET_OPEN_GUARD",
|
||||
"\nTRADING_DAY_RESET_OPEN_GUARD_ENABLED=true\n",
|
||||
),
|
||||
("MAX_ACTIVE_POSITIONS", "\nMAX_ACTIVE_POSITIONS=1\nMANUAL_MIN_PLANNED_RR=1.4\n"),
|
||||
("KEY_CONFIRM_BREAKOUT_BAR", "\nKEY_CONFIRM_BREAKOUT_BAR=-2\nKEY_CONFIRM_BAR=-1\nKEY_VOLUME_MA_BARS=20\nKEY_VOLUME_RATIO_MIN=1.3\nKEY_BREAKOUT_AMP_MIN_PCT=0.03\nKEY_BREAKOUT_AMP_MAX_PCT=0.5\n"),
|
||||
("EXCHANGE_DISPLAY_NAME", "\nEXCHANGE_DISPLAY_NAME=OKX\nOKX_ACCOUNT_LABEL=\n"),
|
||||
("BACKUP_ROOT", "\nBACKUP_ROOT=/root/backups\nBACKUP_RETENTION_DAYS=30\nBACKUP_INSTANCE=crypto_monitor_okx\n"),
|
||||
]:
|
||||
if marker not in okx:
|
||||
okx += block
|
||||
if "TOTAL_CAPITAL=100" in okx and "# TOTAL_CAPITAL" not in okx:
|
||||
okx = okx.replace("TOTAL_CAPITAL=100", "# TOTAL_CAPITAL=100 # 已弃用,资金展示读交易所")
|
||||
okx_path.write_text(okx, encoding="utf-8")
|
||||
print("updated .env.example")
|
||||
|
||||
|
||||
def copy_scripts_docs():
|
||||
for name in ("backup_data.sh", "install_backup_cron.sh"):
|
||||
s = BIN / "scripts" / name
|
||||
d = OKX / "scripts" / name
|
||||
if s.is_file():
|
||||
d.parent.mkdir(parents=True, exist_ok=True)
|
||||
content = s.read_text(encoding="utf-8").replace("crypto_monitor_binance", "crypto_monitor_okx")
|
||||
content = content.replace("BINANCE", "OKX")
|
||||
d.write_text(content, encoding="utf-8")
|
||||
v = BIN / "scripts" / "verify_binance_funding.py"
|
||||
if v.is_file():
|
||||
t = v.read_text(encoding="utf-8")
|
||||
t = t.replace("binance", "okx").replace("BINANCE", "OKX").replace("verify_binance", "verify_okx")
|
||||
(OKX / "scripts" / "verify_okx_funding.py").write_text(t, encoding="utf-8")
|
||||
doc = BIN / "关键位自动下单说明.md"
|
||||
if doc.is_file() and not (OKX / "关键位自动下单说明.md").exists():
|
||||
shutil.copy2(doc, OKX / "关键位自动下单说明.md")
|
||||
eco = OKX / "ecosystem.config.cjs"
|
||||
if eco.is_file():
|
||||
t = eco.read_text(encoding="utf-8").replace("GATE_SOCKS_PROXY", "OKX_SOCKS_PROXY")
|
||||
eco.write_text(t, encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
copy_templates()
|
||||
patch_app()
|
||||
copy_env_example()
|
||||
copy_scripts_docs()
|
||||
print("done")
|
||||
Reference in New Issue
Block a user