diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py
index ebb269b..8e50f0a 100644
--- a/crypto_monitor_binance/app.py
+++ b/crypto_monitor_binance/app.py
@@ -29,7 +29,19 @@ except ImportError:
ImageFont = None # type: ignore
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+_REPO_ROOT = os.path.dirname(BASE_DIR)
+import sys
+if _REPO_ROOT not in sys.path:
+ sys.path.insert(0, _REPO_ROOT)
+from fib_key_monitor_lib import (
+ FIB_KEY_MONITOR_TYPES,
+ calc_fib_plan,
+ fib_invalidate_by_mark,
+ fib_ratio_from_type,
+ is_fib_key_monitor_type,
+ stored_key_signal_type,
+)
def load_env_file(path):
if not os.path.exists(path):
@@ -1238,6 +1250,20 @@ def init_db():
try:
c.execute("ALTER TABLE key_monitors ADD COLUMN breakout_limit_pct REAL DEFAULT 1.5")
except: pass
+ for ddl in (
+ "ALTER TABLE key_monitors ADD COLUMN fib_limit_order_id TEXT",
+ "ALTER TABLE key_monitors ADD COLUMN fib_entry_price REAL",
+ "ALTER TABLE key_monitors ADD COLUMN fib_stop_loss REAL",
+ "ALTER TABLE key_monitors ADD COLUMN fib_take_profit REAL",
+ "ALTER TABLE key_monitors ADD COLUMN fib_order_amount REAL",
+ "ALTER TABLE key_monitors ADD COLUMN fib_margin_capital REAL",
+ "ALTER TABLE key_monitors ADD COLUMN fib_leverage INTEGER",
+ ):
+ try:
+ c.execute(ddl)
+ except Exception:
+ pass
+
try:
c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL")
except Exception:
@@ -2102,7 +2128,9 @@ def order_row_key_signal_type(row):
if "key_signal_type" not in keys:
return None
kst = (row["key_signal_type"] or "").strip()
- return kst if kst in KEY_MONITOR_AUTO_TYPES else None
+ if kst in KEY_MONITOR_AUTO_TYPES or is_fib_key_monitor_type(kst):
+ return kst
+ return None
def exchange_private_api_configured():
@@ -3875,7 +3903,7 @@ def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_
opened_at_ms,
trading_day,
ORDER_MONITOR_TYPE_KEY_AUTO,
- (key_signal_type if key_signal_type in KEY_MONITOR_AUTO_TYPES else None),
+ stored_key_signal_type(key_signal_type),
),
)
new_order_id = int(conn.execute("SELECT last_insert_rowid()").fetchone()[0])
@@ -3907,6 +3935,406 @@ def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_
}
+def _sqlite_row_val(row, key, default=None):
+ try:
+ v = row[key]
+ return default if v is None else v
+ except (KeyError, IndexError, TypeError):
+ return default
+
+
+def get_symbol_mark_price(symbol):
+ """斐波失效判定用标记价。"""
+ ex_sym = normalize_exchange_symbol(symbol)
+ try:
+ ensure_markets_loaded()
+ ticker = exchange.fetch_ticker(ex_sym)
+ m = _coerce_float(ticker.get("mark"), ticker.get("last"))
+ if m is None:
+ info = ticker.get("info") or {}
+ m = _coerce_float(info.get("mark_price"), info.get("last"))
+ if m is not None and m > 0:
+ return float(m)
+ except Exception:
+ pass
+ p = get_price(symbol)
+ return float(p) if p is not None else None
+
+
+def cancel_fib_limit_order(exchange_symbol, order_id):
+ """仅撤销本条斐波限价单,不用 cancel_all。"""
+ if not order_id:
+ return False
+ ok_live, _ = ensure_exchange_live_ready()
+ if not ok_live:
+ return False
+ ensure_markets_loaded()
+ oid = str(order_id)
+ try:
+ exchange.cancel_order(oid, exchange_symbol)
+ return True
+ except Exception:
+ pass
+ try:
+ for o in exchange.fetch_open_orders(exchange_symbol) or []:
+ if str(o.get("id")) == oid:
+ exchange.cancel_order(oid, exchange_symbol)
+ return True
+ except Exception:
+ pass
+ return False
+
+
+def fib_limit_order_status(exchange_symbol, order_id):
+ if not order_id:
+ return "missing"
+ ensure_markets_loaded()
+ oid = str(order_id)
+ try:
+ o = exchange.fetch_order(oid, exchange_symbol)
+ st = (o.get("status") or "").lower()
+ if st in ("closed", "filled"):
+ filled = float(o.get("filled") or 0)
+ if filled > 0 or st == "filled":
+ return "filled"
+ if st in ("canceled", "cancelled", "expired", "rejected"):
+ return "canceled"
+ if st in ("open", "new", "partially_filled"):
+ return "open"
+ except Exception:
+ pass
+ try:
+ for o in exchange.fetch_open_orders(exchange_symbol) or []:
+ if str(o.get("id")) == oid:
+ return "open"
+ except Exception:
+ pass
+ return "unknown"
+
+
+def place_fib_limit_order(exchange_symbol, direction, amount, leverage, limit_price):
+ ensure_markets_loaded()
+ mm = "cross" if BINANCE_MARGIN_MODE in ("cross", "cross_margin") else "isolated"
+ try:
+ exchange.set_margin_mode(mm, exchange_symbol)
+ except Exception:
+ pass
+ exchange.set_leverage(leverage, exchange_symbol)
+ side = "buy" if direction == "long" else "sell"
+ price = round_price_to_exchange(exchange_symbol, float(limit_price))
+ if price is None or price <= 0:
+ raise ValueError("挂单价无效")
+ params = build_binance_order_params(direction, reduce_only=False)
+ return exchange.create_order(exchange_symbol, "limit", side, amount, price, params)
+
+
+def _fib_key_exists_for_symbol(conn, symbol):
+ ph = ",".join("?" * len(FIB_KEY_MONITOR_TYPES))
+ row = conn.execute(
+ f"SELECT id FROM key_monitors WHERE symbol=? AND monitor_type IN ({ph})",
+ (symbol, *tuple(FIB_KEY_MONITOR_TYPES)),
+ ).fetchone()
+ return row is not None
+
+
+def _fib_plan_for_row(row):
+ typ = (row["monitor_type"] or "").strip()
+ ratio = fib_ratio_from_type(typ)
+ if ratio is None:
+ return None
+ return calc_fib_plan(row["direction"], row["upper"], row["lower"], ratio)
+
+
+def _cancel_fib_monitor_limit(row):
+ ex_sym = normalize_exchange_symbol(row["symbol"])
+ oid = _sqlite_row_val(row, "fib_limit_order_id")
+ if oid:
+ cancel_fib_limit_order(ex_sym, oid)
+
+
+def _fib_has_live_position(exchange_symbol, direction):
+ live = get_live_position_contracts(exchange_symbol, direction)
+ return live is not None and float(live) > 0
+
+
+def _insert_order_monitor_from_fib_fill(
+ conn, row, trigger_price, stop_loss, take_profit, amount, leverage, margin_capital,
+ notional_value, position_ratio, base_amount, exchange_order_id, tpsl_attached,
+):
+ symbol = row["symbol"]
+ direction = (row["direction"] or "long").lower()
+ exchange_symbol = normalize_exchange_symbol(symbol)
+ typ = (row["monitor_type"] or "").strip()
+ now = app_now()
+ trading_day = get_trading_day(now)
+ trade_style = (DEFAULT_TRADE_STYLE or "trend").strip().lower()
+ if trade_style not in ("trend", "swing"):
+ trade_style = "trend"
+ risk_percent = max(0.01, float(RISK_PERCENT))
+ risk_amount_final = calc_risk_amount_from_plan(direction, trigger_price, stop_loss, margin_capital, leverage)
+ if risk_amount_final is None:
+ risk_amount_final = round(float(margin_capital) * risk_percent / 100.0, 4)
+ breakeven_rr_trigger = float(BREAKEVEN_RR_TRIGGER)
+ breakeven_offset_pct = float(BREAKEVEN_OFFSET_PCT)
+ breakeven_step_r = float(BREAKEVEN_STEP_R) if float(BREAKEVEN_STEP_R) > 0 else 1.0
+ if direction == "short":
+ breakeven_raw = float(trigger_price) * (1 - breakeven_offset_pct / 100.0)
+ else:
+ breakeven_raw = float(trigger_price) * (1 + breakeven_offset_pct / 100.0)
+ breakeven_price = round_price_to_exchange(exchange_symbol, breakeven_raw)
+ opened_at_bj = app_now_str()
+ opened_at_ms = _to_ms_with_fallback(None, opened_at_bj)
+ conn.execute(
+ "INSERT INTO order_monitors "
+ "(symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, "
+ "margin_capital, leverage, trade_style, risk_percent, risk_amount, "
+ "breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, breakeven_armed, breakeven_price, breakeven_enabled, "
+ "notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type, key_signal_type) "
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
+ (
+ symbol,
+ exchange_symbol,
+ direction,
+ trigger_price,
+ stop_loss,
+ stop_loss,
+ take_profit,
+ margin_capital,
+ leverage,
+ trade_style,
+ risk_percent,
+ risk_amount_final,
+ breakeven_rr_trigger,
+ breakeven_offset_pct,
+ breakeven_step_r,
+ 0,
+ breakeven_price,
+ 1,
+ notional_value,
+ position_ratio,
+ base_amount,
+ amount,
+ exchange_order_id or "",
+ opened_at_bj,
+ opened_at_ms,
+ trading_day,
+ ORDER_MONITOR_TYPE_KEY_AUTO,
+ stored_key_signal_type(typ),
+ ),
+ )
+ new_order_id = int(conn.execute("SELECT last_insert_rowid()").fetchone()[0])
+ return new_order_id
+
+
+def _finalize_fib_key_fill(conn, row):
+ symbol = row["symbol"]
+ direction = (row["direction"] or "long").lower()
+ typ = (row["monitor_type"] or "").strip()
+ ex_sym = normalize_exchange_symbol(symbol)
+ plan = _fib_plan_for_row(row)
+ if not plan:
+ _finalize_key_monitor_one_shot(conn, row, "斐波计划无效", "fib_plan_invalid")
+ return
+ entry_plan, sl_plan, tp_plan = plan
+ sl = float(_sqlite_row_val(row, "fib_stop_loss", sl_plan) or sl_plan)
+ tp = float(_sqlite_row_val(row, "fib_take_profit", tp_plan) or tp_plan)
+ sl_adj = round_price_to_exchange(ex_sym, sl)
+ tp_adj = round_price_to_exchange(ex_sym, tp)
+ if sl_adj is not None:
+ sl = float(sl_adj)
+ if tp_adj is not None:
+ tp = float(tp_adj)
+ amount = float(_sqlite_row_val(row, "fib_order_amount") or 0)
+ leverage = int(_sqlite_row_val(row, "fib_leverage") or infer_leverage(symbol) or 5)
+ margin_capital = float(_sqlite_row_val(row, "fib_margin_capital") or 0)
+ oid = _sqlite_row_val(row, "fib_limit_order_id")
+ entry_px = float(_sqlite_row_val(row, "fib_entry_price", entry_plan) or entry_plan)
+ trigger_price = entry_px
+ if oid:
+ try:
+ o = exchange.fetch_order(str(oid), ex_sym)
+ trigger_price = resolve_order_entry_price(o, ex_sym, entry_px)
+ except Exception:
+ pass
+ tr_adj = round_price_to_exchange(ex_sym, trigger_price)
+ if tr_adj is not None:
+ trigger_price = float(tr_adj)
+ if amount <= 0:
+ live_amt = get_live_position_contracts(ex_sym, direction)
+ amount = float(live_amt or 0)
+ if amount <= 0:
+ send_wechat_msg(
+ f"# ❌ {symbol} 斐波成交后处理失败\n"
+ f"**账户:{_wechat_account_label()}**\n"
+ f"- 无法取得持仓/下单数量,未挂 TP/SL\n"
+ )
+ return
+ ok, reason = precheck_risk(conn, symbol, direction)
+ if not ok:
+ send_wechat_msg(
+ f"# ❌ {symbol} 斐波成交后风控拒绝\n"
+ f"**账户:{_wechat_account_label()}**\n"
+ f"- 类型:{typ}\n"
+ f"- 原因:{reason}\n"
+ f"- 请手动处理仓位与挂单\n"
+ )
+ return
+ tpsl_attached = False
+ try:
+ _binance_place_tp_sl_orders(ex_sym, direction, amount, sl, tp)
+ tpsl_attached = True
+ except Exception as e:
+ send_wechat_msg(
+ f"# ❌ {symbol} 斐波成交后挂 TP/SL 失败\n"
+ f"**账户:{_wechat_account_label()}**\n"
+ f"- 错误:{friendly_exchange_error(e)}\n"
+ f"- 请手动补挂止盈止损\n"
+ )
+ return
+ contract_size = get_contract_size(ex_sym)
+ base_amount = round(float(amount) * contract_size, 8)
+ notional_value = round(float(margin_capital) * leverage, 4) if margin_capital else 0
+ session_row = ensure_session(conn, get_trading_day(app_now()))
+ capital_base = float(session_row["current_capital"] or 0)
+ position_ratio = round(margin_capital / capital_base * 100, 2) if capital_base and margin_capital else 0
+ planned_rr = calc_rr_ratio(direction, trigger_price, sl, tp)
+ new_order_id = _insert_order_monitor_from_fib_fill(
+ conn, row, trigger_price, sl, tp, amount, leverage, margin_capital,
+ notional_value, position_ratio, base_amount, oid, tpsl_attached,
+ )
+ rr_txt = format_wechat_scalar_2dp(planned_rr) if planned_rr is not None else "-"
+ succ = (
+ f"# ✅ {symbol} 斐波限价成交\n"
+ f"**账户:{_wechat_account_label()}**\n"
+ f"- 来源:{ORDER_MONITOR_TYPE_KEY_AUTO}(限价 @ E)\n"
+ f"- 类型:{typ}|{_wechat_direction_text(direction)}\n"
+ f"- 订单 ID:**{new_order_id}**\n"
+ f"- 成交价:{format_price_for_symbol(symbol, trigger_price)}\n"
+ f"- 止损:{format_wechat_scalar_2dp(sl)}|止盈:{format_price_for_symbol(symbol, tp)}\n"
+ f"- 计划 RR:{rr_txt}:1\n"
+ f"- {'已挂交易所 TP/SL' if tpsl_attached else 'TP/SL 未挂上'}\n"
+ )
+ send_wechat_msg(succ)
+ _finalize_key_monitor_one_shot(conn, row, succ, "fib_filled")
+
+
+def check_fib_key_monitors():
+ conn = get_db()
+ rows = conn.execute("SELECT * FROM key_monitors").fetchall()
+ for r in rows:
+ typ = (r["monitor_type"] or "").strip()
+ if not is_fib_key_monitor_type(typ):
+ continue
+ symbol = r["symbol"]
+ direction = (r["direction"] or "long").lower()
+ ex_sym = normalize_exchange_symbol(symbol)
+ up, low = float(r["upper"]), float(r["lower"])
+ oid = _sqlite_row_val(r, "fib_limit_order_id")
+ mark = get_symbol_mark_price(symbol)
+ if mark is None:
+ continue
+ status = fib_limit_order_status(ex_sym, oid) if oid else "missing"
+ if status == "filled" or (status != "open" and _fib_has_live_position(ex_sym, direction)):
+ _finalize_fib_key_fill(conn, r)
+ continue
+ if status == "open":
+ if fib_invalidate_by_mark(direction, mark, up, low):
+ _cancel_fib_monitor_limit(r)
+ msg = (
+ f"# ⚠️ {symbol} 斐波监控失效\n"
+ f"**账户:{_wechat_account_label()}**\n"
+ f"- 类型:{typ}|{_wechat_direction_text(direction)}\n"
+ f"- 标记价 {format_price_for_symbol(symbol, mark)} 已触达止盈侧(未成交),已撤限价单\n"
+ )
+ send_wechat_msg(msg)
+ _finalize_key_monitor_one_shot(conn, r, msg, "fib_invalidate")
+ continue
+ if status in ("canceled", "missing", "unknown") and fib_invalidate_by_mark(direction, mark, up, low):
+ msg = (
+ f"# ⚠️ {symbol} 斐波监控失效(限价已不在挂单)\n"
+ f"**账户:{_wechat_account_label()}**\n"
+ f"- 标记价触达止盈侧,本条已结案\n"
+ )
+ send_wechat_msg(msg)
+ _finalize_key_monitor_one_shot(conn, r, msg, "fib_invalidate")
+ conn.commit()
+ conn.close()
+
+
+def _add_fib_key_monitor(conn, symbol, direction_sel, mt, upper_px, lower_px):
+ if _fib_key_exists_for_symbol(conn, symbol):
+ return False, f"{symbol} 已有斐波监控(同币仅允许一条 0.618/0.786)"
+ ratio = fib_ratio_from_type(mt)
+ plan = calc_fib_plan(direction_sel, upper_px, lower_px, ratio)
+ if not plan:
+ return False, "斐波上下沿无效(需上沿 H > 下沿 L)"
+ entry, sl, tp = plan
+ ex_sym = normalize_exchange_symbol(symbol)
+ entry = round_price_to_exchange(ex_sym, entry)
+ sl = round_price_to_exchange(ex_sym, sl)
+ tp = round_price_to_exchange(ex_sym, tp)
+ if entry is None or sl is None or tp is None:
+ return False, "斐波价位经交易所精度舍入后无效"
+ entry, sl, tp = float(entry), float(sl), float(tp)
+ planned_rr = calc_rr_ratio(direction_sel, entry, sl, tp)
+ if planned_rr is None or planned_rr <= KEY_AUTO_MIN_PLANNED_RR:
+ fmt_rr = f"{planned_rr:.4f}" if planned_rr is not None else "无法计算"
+ return False, f"斐波计划盈亏比 {fmt_rr}:1 未达要求(>{KEY_AUTO_MIN_PLANNED_RR}:1)"
+ ok, reason = precheck_risk(conn, symbol, direction_sel)
+ if not ok:
+ return False, reason
+ ok_live, reason_live = ensure_exchange_live_ready()
+ if not ok_live:
+ return False, reason_live
+ now = app_now()
+ trading_day = get_trading_day(now)
+ session_row = ensure_session(conn, trading_day)
+ _, trading_capital_live = get_exchange_capitals(force=True)
+ live_capital = float(trading_capital_live) if trading_capital_live is not None else float(session_row["current_capital"])
+ capital_base = resolve_capital_base_for_key_open(conn, trading_day, live_capital)
+ default_leverage = get_synced_leverage(ex_sym, direction_sel) or infer_leverage(symbol)
+ leverage = int(default_leverage) if default_leverage else 5
+ if leverage <= 0:
+ leverage = 5
+ available_usdt = get_available_trading_usdt()
+ risk_fraction = calc_risk_fraction(direction_sel, entry, sl)
+ if risk_fraction is None:
+ return False, "止损方向不合法(相对挂单价 E);请核对上下沿与方向"
+ 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)
+ margin_capital = round(notional_value / leverage, 4)
+ if capital_base and margin_capital > capital_base:
+ return False, "以损定仓后保证金超过当前交易资金"
+ if available_usdt is not None:
+ max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), 4)
+ if margin_capital > max_margin:
+ return (
+ False,
+ f"保证金不足:交易账户可用约 {round(available_usdt, 2)}U,当前最多建议 {round(max_margin, 2)}U",
+ )
+ try:
+ amount, _ = prepare_order_amount(ex_sym, margin_capital, leverage, entry)
+ order_resp = place_fib_limit_order(ex_sym, direction_sel, amount, leverage, entry)
+ oid = str(order_resp.get("id") or "")
+ if not oid:
+ return False, "交易所未返回限价单 ID"
+ except Exception as e:
+ return False, friendly_exchange_error(e, available_usdt=available_usdt)
+ conn.execute(
+ "INSERT INTO key_monitors "
+ "(symbol, monitor_type, direction, upper, lower, "
+ "fib_limit_order_id, fib_entry_price, fib_stop_loss, fib_take_profit, "
+ "fib_order_amount, fib_margin_capital, fib_leverage) "
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
+ (
+ symbol, mt, direction_sel, upper_px, lower_px,
+ oid, entry, sl, tp, float(amount), margin_capital, leverage,
+ ),
+ )
+ return True, None
+
+
# 关键位监控(箱体/收敛可自动开仓;阻力/支撑位仅单次提醒结案)
def check_key_monitors():
conn = get_db()
@@ -3914,6 +4342,8 @@ def check_key_monitors():
for r in rows:
sym, typ_raw, up, low = r["symbol"], r["monitor_type"], r["upper"], r["lower"]
typ = (typ_raw or "").strip()
+ if is_fib_key_monitor_type(typ):
+ continue
direction = (r["direction"] or "long").lower()
try:
checks = _key_hard_checks(sym, direction, up, low, typ)
@@ -4421,6 +4851,7 @@ def background_task():
conn.commit()
conn.close()
force_close_before_reset()
+ check_fib_key_monitors()
check_key_monitors()
check_order_monitors()
except:
@@ -4707,7 +5138,8 @@ def render_main_page(page="trade"):
key_gate_rule_text = (
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"自动开仓盈亏比 > {KEY_AUTO_MIN_PLANNED_RR}:1|日成交量排名前 {KEY_DAILY_VOLUME_RANK_MAX}|"
+ f"斐波:添加后立即挂限价 @ E,失效按标记价触达 H/L(未成交撤单)"
)
conn.close()
return render_template(
@@ -4823,7 +5255,9 @@ def api_account_snapshot():
@login_required
def api_price_snapshot():
conn = get_db()
- key_rows = conn.execute("SELECT id,symbol,monitor_type,direction,upper,lower FROM key_monitors").fetchall()
+ key_rows = conn.execute(
+ "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,exchange_symbol,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage FROM order_monitors WHERE status='active'"
).fetchall()
@@ -4851,18 +5285,33 @@ def api_price_snapshot():
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"])
gate = None
- 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
+ if is_fib:
+ direction = (r["direction"] or "long").lower()
+ 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 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')}"
+ 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 = (
@@ -4897,7 +5346,7 @@ def api_price_snapshot():
"lower_diff": lower_diff,
"lower_pct": lower_pct,
"gate_summary": gate_summary,
- "gate_ok": bool(gate and gate.get("ok")),
+ "gate_ok": fib_gate_ok if is_fib else bool(gate and gate.get("ok")),
"gate_metrics": gate_metrics,
})
@@ -5337,9 +5786,13 @@ def add_key():
flash("请选择做多或做空")
return redirect("/key_monitor")
mt = (d.get("type") or "").strip()
- allowed_types = tuple(KEY_MONITOR_AUTO_TYPES) + tuple(KEY_MONITOR_ALERT_ONLY_TYPES)
+ allowed_types = (
+ tuple(KEY_MONITOR_AUTO_TYPES)
+ + tuple(KEY_MONITOR_ALERT_ONLY_TYPES)
+ + tuple(FIB_KEY_MONITOR_TYPES)
+ )
if mt not in allowed_types:
- flash("监控类型无效,请选择:箱体突破、收敛突破、关键阻力位、关键支撑位")
+ flash("监控类型无效")
return redirect("/key_monitor")
rank, total = _daily_volume_rank(symbol)
if rank is None:
@@ -5367,6 +5820,15 @@ def add_key():
lw = round_price_to_exchange(ex_sym_key, float(d["lower"]))
upper_px = float(uh) if uh is not None else float(d["upper"])
lower_px = float(lw) if lw is not None else float(d["lower"])
+ if is_fib_key_monitor_type(mt):
+ ok_fib, err_fib = _add_fib_key_monitor(conn, symbol, direction_sel, mt, upper_px, lower_px)
+ conn.commit()
+ conn.close()
+ if not ok_fib:
+ flash(err_fib or "斐波监控添加失败")
+ return redirect("/key_monitor")
+ flash(f"斐波监控已添加,限价单已挂出({symbol} 日成交量排名 {rank}/{total})")
+ return redirect("/key_monitor")
conn.execute(
"INSERT INTO key_monitors (symbol,monitor_type,direction,upper,lower) VALUES (?,?,?,?,?)",
(symbol, mt, direction_sel, upper_px, lower_px),
@@ -5712,6 +6174,8 @@ def delete_key_monitor(kid):
if not row:
conn.close()
return jsonify({"ok": False, "error": "not_found"})
+ if is_fib_key_monitor_type(row["monitor_type"]):
+ _cancel_fib_monitor_limit(row)
insert_key_monitor_history(conn, row, int(row["notification_count"] or 0), None, "manual")
cur = conn.execute("DELETE FROM key_monitors WHERE id=?", (kid,))
conn.commit()
@@ -5735,6 +6199,8 @@ def del_key(id):
conn = get_db()
row = conn.execute("SELECT * FROM key_monitors WHERE id=?", (id,)).fetchone()
if row:
+ if is_fib_key_monitor_type(row["monitor_type"]):
+ _cancel_fib_monitor_limit(row)
insert_key_monitor_history(conn, row, int(row["notification_count"] or 0), None, "manual")
conn.execute("DELETE FROM key_monitors WHERE id=?", (id,))
conn.commit()
diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html
index 672cd1f..77f6ea0 100644
--- a/crypto_monitor_binance/templates/index.html
+++ b/crypto_monitor_binance/templates/index.html
@@ -239,6 +239,8 @@
@@ -264,6 +266,7 @@
上沿: {{ k.upper }}
下沿: {{ k.lower }}
+ {% if k.fib_entry_price %}挂E: {{ k.fib_entry_price }}{% endif %}
已提醒: {{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}
diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py
index 38f17be..e260b3b 100644
--- a/crypto_monitor_gate/app.py
+++ b/crypto_monitor_gate/app.py
@@ -29,6 +29,19 @@ except ImportError:
ImageFont = None # type: ignore
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+_REPO_ROOT = os.path.dirname(BASE_DIR)
+import sys
+
+if _REPO_ROOT not in sys.path:
+ sys.path.insert(0, _REPO_ROOT)
+from fib_key_monitor_lib import (
+ FIB_KEY_MONITOR_TYPES,
+ calc_fib_plan,
+ fib_invalidate_by_mark,
+ fib_ratio_from_type,
+ is_fib_key_monitor_type,
+ stored_key_signal_type,
+)
def load_env_file(path):
@@ -1240,6 +1253,19 @@ def init_db():
try:
c.execute("ALTER TABLE key_monitors ADD COLUMN breakout_limit_pct REAL DEFAULT 1.5")
except: pass
+ for ddl in (
+ "ALTER TABLE key_monitors ADD COLUMN fib_limit_order_id TEXT",
+ "ALTER TABLE key_monitors ADD COLUMN fib_entry_price REAL",
+ "ALTER TABLE key_monitors ADD COLUMN fib_stop_loss REAL",
+ "ALTER TABLE key_monitors ADD COLUMN fib_take_profit REAL",
+ "ALTER TABLE key_monitors ADD COLUMN fib_order_amount REAL",
+ "ALTER TABLE key_monitors ADD COLUMN fib_margin_capital REAL",
+ "ALTER TABLE key_monitors ADD COLUMN fib_leverage INTEGER",
+ ):
+ try:
+ c.execute(ddl)
+ except Exception:
+ pass
try:
c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL")
except Exception:
@@ -2080,7 +2106,9 @@ def order_row_key_signal_type(row):
if "key_signal_type" not in keys:
return None
kst = (row["key_signal_type"] or "").strip()
- return kst if kst in KEY_MONITOR_AUTO_TYPES else None
+ if kst in KEY_MONITOR_AUTO_TYPES or is_fib_key_monitor_type(kst):
+ return kst
+ return None
def exchange_private_api_configured():
@@ -4000,7 +4028,7 @@ def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_
opened_at_ms,
trading_day,
ORDER_MONITOR_TYPE_KEY_AUTO,
- (key_signal_type if key_signal_type in KEY_MONITOR_AUTO_TYPES else None),
+ stored_key_signal_type(key_signal_type),
),
)
new_order_id = int(conn.execute("SELECT last_insert_rowid()").fetchone()[0])
@@ -4033,6 +4061,402 @@ def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_
}
+def _sqlite_row_val(row, key, default=None):
+ try:
+ v = row[key]
+ return default if v is None else v
+ except (KeyError, IndexError, TypeError):
+ return default
+
+
+def get_symbol_mark_price(symbol):
+ """斐波失效判定用标记价。"""
+ ex_sym = normalize_exchange_symbol(symbol)
+ try:
+ ensure_markets_loaded()
+ ticker = exchange.fetch_ticker(ex_sym)
+ m = _coerce_float(ticker.get("mark"), ticker.get("last"))
+ if m is None:
+ info = ticker.get("info") or {}
+ m = _coerce_float(info.get("mark_price"), info.get("last"))
+ if m is not None and m > 0:
+ return float(m)
+ except Exception:
+ pass
+ p = get_price(symbol)
+ return float(p) if p is not None else None
+
+
+def cancel_fib_limit_order(exchange_symbol, order_id):
+ """仅撤销本条斐波限价单,不用 cancel_all。"""
+ if not order_id:
+ return False
+ ok_live, _ = ensure_exchange_live_ready()
+ if not ok_live:
+ return False
+ ensure_markets_loaded()
+ oid = str(order_id)
+ try:
+ exchange.cancel_order(oid, exchange_symbol)
+ return True
+ except Exception:
+ pass
+ try:
+ for o in exchange.fetch_open_orders(exchange_symbol) or []:
+ if str(o.get("id")) == oid:
+ exchange.cancel_order(oid, exchange_symbol)
+ return True
+ except Exception:
+ pass
+ return False
+
+
+def fib_limit_order_status(exchange_symbol, order_id):
+ if not order_id:
+ return "missing"
+ ensure_markets_loaded()
+ oid = str(order_id)
+ try:
+ o = exchange.fetch_order(oid, exchange_symbol)
+ st = (o.get("status") or "").lower()
+ if st in ("closed", "filled"):
+ filled = float(o.get("filled") or 0)
+ if filled > 0 or st == "filled":
+ return "filled"
+ if st in ("canceled", "cancelled", "expired", "rejected"):
+ return "canceled"
+ if st in ("open", "new", "partially_filled"):
+ return "open"
+ except Exception:
+ pass
+ try:
+ for o in exchange.fetch_open_orders(exchange_symbol) or []:
+ if str(o.get("id")) == oid:
+ return "open"
+ except Exception:
+ pass
+ return "unknown"
+
+
+def place_fib_limit_order(exchange_symbol, direction, amount, leverage, limit_price):
+ ensure_markets_loaded()
+ exchange.set_leverage(leverage, exchange_symbol)
+ side = "buy" if direction == "long" else "sell"
+ price = round_price_to_exchange(exchange_symbol, float(limit_price))
+ if price is None or price <= 0:
+ raise ValueError("挂单价无效")
+ params = build_gate_order_params(direction, reduce_only=False)
+ return exchange.create_order(exchange_symbol, "limit", side, amount, price, params)
+
+
+def _fib_key_exists_for_symbol(conn, symbol):
+ ph = ",".join("?" * len(FIB_KEY_MONITOR_TYPES))
+ row = conn.execute(
+ f"SELECT id FROM key_monitors WHERE symbol=? AND monitor_type IN ({ph})",
+ (symbol, *tuple(FIB_KEY_MONITOR_TYPES)),
+ ).fetchone()
+ return row is not None
+
+
+def _fib_plan_for_row(row):
+ typ = (row["monitor_type"] or "").strip()
+ ratio = fib_ratio_from_type(typ)
+ if ratio is None:
+ return None
+ return calc_fib_plan(row["direction"], row["upper"], row["lower"], ratio)
+
+
+def _cancel_fib_monitor_limit(row):
+ ex_sym = normalize_exchange_symbol(row["symbol"])
+ oid = _sqlite_row_val(row, "fib_limit_order_id")
+ if oid:
+ cancel_fib_limit_order(ex_sym, oid)
+
+
+def _fib_has_live_position(exchange_symbol, direction):
+ live = get_live_position_contracts(exchange_symbol, direction)
+ return live is not None and float(live) > 0
+
+
+def _insert_order_monitor_from_fib_fill(
+ conn, row, trigger_price, stop_loss, take_profit, amount, leverage, margin_capital,
+ notional_value, position_ratio, base_amount, exchange_order_id, tpsl_attached,
+):
+ symbol = row["symbol"]
+ direction = (row["direction"] or "long").lower()
+ exchange_symbol = normalize_exchange_symbol(symbol)
+ typ = (row["monitor_type"] or "").strip()
+ now = app_now()
+ trading_day = get_trading_day(now)
+ trade_style = (DEFAULT_TRADE_STYLE or "trend").strip().lower()
+ if trade_style not in ("trend", "swing"):
+ trade_style = "trend"
+ risk_percent = max(0.01, float(RISK_PERCENT))
+ risk_amount_final = calc_risk_amount_from_plan(direction, trigger_price, stop_loss, margin_capital, leverage)
+ if risk_amount_final is None:
+ risk_amount_final = round(float(margin_capital) * risk_percent / 100.0, 4)
+ breakeven_rr_trigger = float(BREAKEVEN_RR_TRIGGER)
+ breakeven_offset_pct = float(BREAKEVEN_OFFSET_PCT)
+ breakeven_step_r = float(BREAKEVEN_STEP_R) if float(BREAKEVEN_STEP_R) > 0 else 1.0
+ if direction == "short":
+ breakeven_raw = float(trigger_price) * (1 - breakeven_offset_pct / 100.0)
+ else:
+ breakeven_raw = float(trigger_price) * (1 + breakeven_offset_pct / 100.0)
+ breakeven_price = round_price_to_exchange(exchange_symbol, breakeven_raw)
+ opened_at_bj = app_now_str()
+ opened_at_ms = _to_ms_with_fallback(None, opened_at_bj)
+ conn.execute(
+ "INSERT INTO order_monitors "
+ "(symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, "
+ "margin_capital, leverage, trade_style, risk_percent, risk_amount, "
+ "breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, breakeven_armed, breakeven_price, breakeven_enabled, "
+ "notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type, key_signal_type) "
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
+ (
+ symbol,
+ exchange_symbol,
+ direction,
+ trigger_price,
+ stop_loss,
+ stop_loss,
+ take_profit,
+ margin_capital,
+ leverage,
+ trade_style,
+ risk_percent,
+ risk_amount_final,
+ breakeven_rr_trigger,
+ breakeven_offset_pct,
+ breakeven_step_r,
+ 0,
+ breakeven_price,
+ 1,
+ notional_value,
+ position_ratio,
+ base_amount,
+ amount,
+ exchange_order_id or "",
+ opened_at_bj,
+ opened_at_ms,
+ trading_day,
+ ORDER_MONITOR_TYPE_KEY_AUTO,
+ stored_key_signal_type(typ),
+ ),
+ )
+ new_order_id = int(conn.execute("SELECT last_insert_rowid()").fetchone()[0])
+ try_persist_exchange_margin_for_order(conn, new_order_id, exchange_symbol, direction, order_leverage=leverage)
+ return new_order_id
+
+
+def _finalize_fib_key_fill(conn, row):
+ symbol = row["symbol"]
+ direction = (row["direction"] or "long").lower()
+ typ = (row["monitor_type"] or "").strip()
+ ex_sym = normalize_exchange_symbol(symbol)
+ plan = _fib_plan_for_row(row)
+ if not plan:
+ _finalize_key_monitor_one_shot(conn, row, "斐波计划无效", "fib_plan_invalid")
+ return
+ entry_plan, sl_plan, tp_plan = plan
+ sl = float(_sqlite_row_val(row, "fib_stop_loss", sl_plan) or sl_plan)
+ tp = float(_sqlite_row_val(row, "fib_take_profit", tp_plan) or tp_plan)
+ sl_adj = round_price_to_exchange(ex_sym, sl)
+ tp_adj = round_price_to_exchange(ex_sym, tp)
+ if sl_adj is not None:
+ sl = float(sl_adj)
+ if tp_adj is not None:
+ tp = float(tp_adj)
+ amount = float(_sqlite_row_val(row, "fib_order_amount") or 0)
+ leverage = int(_sqlite_row_val(row, "fib_leverage") or infer_leverage(symbol) or 5)
+ margin_capital = float(_sqlite_row_val(row, "fib_margin_capital") or 0)
+ oid = _sqlite_row_val(row, "fib_limit_order_id")
+ entry_px = float(_sqlite_row_val(row, "fib_entry_price", entry_plan) or entry_plan)
+ trigger_price = entry_px
+ if oid:
+ try:
+ o = exchange.fetch_order(str(oid), ex_sym)
+ trigger_price = resolve_order_entry_price(o, ex_sym, entry_px)
+ except Exception:
+ pass
+ tr_adj = round_price_to_exchange(ex_sym, trigger_price)
+ if tr_adj is not None:
+ trigger_price = float(tr_adj)
+ if amount <= 0:
+ live_amt = get_live_position_contracts(ex_sym, direction)
+ amount = float(live_amt or 0)
+ if amount <= 0:
+ send_wechat_msg(
+ f"# ❌ {symbol} 斐波成交后处理失败\n"
+ f"**账户:{_wechat_account_label()}**\n"
+ f"- 无法取得持仓/下单数量,未挂 TP/SL\n"
+ )
+ return
+ ok, reason = precheck_risk(conn, symbol, direction)
+ if not ok:
+ send_wechat_msg(
+ f"# ❌ {symbol} 斐波成交后风控拒绝\n"
+ f"**账户:{_wechat_account_label()}**\n"
+ f"- 类型:{typ}\n"
+ f"- 原因:{reason}\n"
+ f"- 请手动处理仓位与挂单\n"
+ )
+ return
+ tpsl_attached = False
+ try:
+ _gate_place_tp_sl_orders(ex_sym, direction, amount, sl, tp)
+ tpsl_attached = True
+ except Exception as e:
+ send_wechat_msg(
+ f"# ❌ {symbol} 斐波成交后挂 TP/SL 失败\n"
+ f"**账户:{_wechat_account_label()}**\n"
+ f"- 错误:{friendly_exchange_error(e)}\n"
+ f"- 请手动补挂止盈止损\n"
+ )
+ return
+ contract_size = get_contract_size(ex_sym)
+ base_amount = round(float(amount) * contract_size, 8)
+ notional_value = round(float(margin_capital) * leverage, 4) if margin_capital else 0
+ session_row = ensure_session(conn, get_trading_day(app_now()))
+ capital_base = float(session_row["current_capital"] or 0)
+ position_ratio = round(margin_capital / capital_base * 100, 2) if capital_base and margin_capital else 0
+ planned_rr = calc_rr_ratio(direction, trigger_price, sl, tp)
+ new_order_id = _insert_order_monitor_from_fib_fill(
+ conn, row, trigger_price, sl, tp, amount, leverage, margin_capital,
+ notional_value, position_ratio, base_amount, oid, tpsl_attached,
+ )
+ rr_txt = format_wechat_scalar_2dp(planned_rr) if planned_rr is not None else "-"
+ succ = (
+ f"# ✅ {symbol} 斐波限价成交\n"
+ f"**账户:{_wechat_account_label()}**\n"
+ f"- 来源:{ORDER_MONITOR_TYPE_KEY_AUTO}(限价 @ E)\n"
+ f"- 类型:{typ}|{_wechat_direction_text(direction)}\n"
+ f"- 订单 ID:**{new_order_id}**\n"
+ f"- 成交价:{format_price_for_symbol(symbol, trigger_price)}\n"
+ f"- 止损:{format_wechat_scalar_2dp(sl)}|止盈:{format_price_for_symbol(symbol, tp)}\n"
+ f"- 计划 RR:{rr_txt}:1\n"
+ f"- {'已挂交易所 TP/SL' if tpsl_attached else 'TP/SL 未挂上'}\n"
+ )
+ send_wechat_msg(succ)
+ _finalize_key_monitor_one_shot(conn, row, succ, "fib_filled")
+
+
+def check_fib_key_monitors():
+ conn = get_db()
+ rows = conn.execute("SELECT * FROM key_monitors").fetchall()
+ for r in rows:
+ typ = (r["monitor_type"] or "").strip()
+ if not is_fib_key_monitor_type(typ):
+ continue
+ symbol = r["symbol"]
+ direction = (r["direction"] or "long").lower()
+ ex_sym = normalize_exchange_symbol(symbol)
+ up, low = float(r["upper"]), float(r["lower"])
+ oid = _sqlite_row_val(r, "fib_limit_order_id")
+ mark = get_symbol_mark_price(symbol)
+ if mark is None:
+ continue
+ status = fib_limit_order_status(ex_sym, oid) if oid else "missing"
+ if status == "filled" or (status != "open" and _fib_has_live_position(ex_sym, direction)):
+ _finalize_fib_key_fill(conn, r)
+ continue
+ if status == "open":
+ if fib_invalidate_by_mark(direction, mark, up, low):
+ _cancel_fib_monitor_limit(r)
+ msg = (
+ f"# ⚠️ {symbol} 斐波监控失效\n"
+ f"**账户:{_wechat_account_label()}**\n"
+ f"- 类型:{typ}|{_wechat_direction_text(direction)}\n"
+ f"- 标记价 {format_price_for_symbol(symbol, mark)} 已触达止盈侧(未成交),已撤限价单\n"
+ )
+ send_wechat_msg(msg)
+ _finalize_key_monitor_one_shot(conn, r, msg, "fib_invalidate")
+ continue
+ if status in ("canceled", "missing", "unknown") and fib_invalidate_by_mark(direction, mark, up, low):
+ msg = (
+ f"# ⚠️ {symbol} 斐波监控失效(限价已不在挂单)\n"
+ f"**账户:{_wechat_account_label()}**\n"
+ f"- 标记价触达止盈侧,本条已结案\n"
+ )
+ send_wechat_msg(msg)
+ _finalize_key_monitor_one_shot(conn, r, msg, "fib_invalidate")
+ conn.commit()
+ conn.close()
+
+
+def _add_fib_key_monitor(conn, symbol, direction_sel, mt, upper_px, lower_px):
+ if _fib_key_exists_for_symbol(conn, symbol):
+ return False, f"{symbol} 已有斐波监控(同币仅允许一条 0.618/0.786)"
+ ratio = fib_ratio_from_type(mt)
+ plan = calc_fib_plan(direction_sel, upper_px, lower_px, ratio)
+ if not plan:
+ return False, "斐波上下沿无效(需上沿 H > 下沿 L)"
+ entry, sl, tp = plan
+ ex_sym = normalize_exchange_symbol(symbol)
+ entry = round_price_to_exchange(ex_sym, entry)
+ sl = round_price_to_exchange(ex_sym, sl)
+ tp = round_price_to_exchange(ex_sym, tp)
+ if entry is None or sl is None or tp is None:
+ return False, "斐波价位经交易所精度舍入后无效"
+ entry, sl, tp = float(entry), float(sl), float(tp)
+ planned_rr = calc_rr_ratio(direction_sel, entry, sl, tp)
+ if planned_rr is None or planned_rr <= KEY_AUTO_MIN_PLANNED_RR:
+ fmt_rr = f"{planned_rr:.4f}" if planned_rr is not None else "无法计算"
+ return False, f"斐波计划盈亏比 {fmt_rr}:1 未达要求(>{KEY_AUTO_MIN_PLANNED_RR}:1)"
+ ok, reason = precheck_risk(conn, symbol, direction_sel)
+ if not ok:
+ return False, reason
+ ok_live, reason_live = ensure_exchange_live_ready()
+ if not ok_live:
+ return False, reason_live
+ now = app_now()
+ trading_day = get_trading_day(now)
+ session_row = ensure_session(conn, trading_day)
+ _, trading_capital_live = get_exchange_capitals(force=True)
+ live_capital = float(trading_capital_live) if trading_capital_live is not None else float(session_row["current_capital"])
+ capital_base = resolve_capital_base_for_key_open(conn, trading_day, live_capital)
+ default_leverage = get_synced_leverage(ex_sym, direction_sel) or infer_leverage(symbol)
+ leverage = int(default_leverage) if default_leverage else 5
+ if leverage <= 0:
+ leverage = 5
+ available_usdt = get_available_trading_usdt()
+ risk_fraction = calc_risk_fraction(direction_sel, entry, sl)
+ if risk_fraction is None:
+ return False, "止损方向不合法(相对挂单价 E);请核对上下沿与方向"
+ 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)
+ margin_capital = round(notional_value / leverage, 4)
+ if capital_base and margin_capital > capital_base:
+ return False, "以损定仓后保证金超过当前交易资金"
+ if available_usdt is not None:
+ max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), 4)
+ if margin_capital > max_margin:
+ return (
+ False,
+ f"保证金不足:交易账户可用约 {round(available_usdt, 2)}U,当前最多建议 {round(max_margin, 2)}U",
+ )
+ try:
+ amount, _ = prepare_order_amount(ex_sym, margin_capital, leverage, entry)
+ order_resp = place_fib_limit_order(ex_sym, direction_sel, amount, leverage, entry)
+ oid = str(order_resp.get("id") or "")
+ if not oid:
+ return False, "交易所未返回限价单 ID"
+ except Exception as e:
+ return False, friendly_exchange_error(e, available_usdt=available_usdt)
+ conn.execute(
+ "INSERT INTO key_monitors "
+ "(symbol, monitor_type, direction, upper, lower, "
+ "fib_limit_order_id, fib_entry_price, fib_stop_loss, fib_take_profit, "
+ "fib_order_amount, fib_margin_capital, fib_leverage) "
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
+ (
+ symbol, mt, direction_sel, upper_px, lower_px,
+ oid, entry, sl, tp, float(amount), margin_capital, leverage,
+ ),
+ )
+ return True, None
+
+
# 关键位监控(箱体/收敛可自动开仓;阻力/支撑位仅单次提醒结案)
def check_key_monitors():
conn = get_db()
@@ -4040,6 +4464,8 @@ def check_key_monitors():
for r in rows:
sym, typ_raw, up, low = r["symbol"], r["monitor_type"], r["upper"], r["lower"]
typ = (typ_raw or "").strip()
+ if is_fib_key_monitor_type(typ):
+ continue
direction = (r["direction"] or "long").lower()
try:
checks = _key_hard_checks(sym, direction, up, low, typ)
@@ -4563,6 +4989,7 @@ def background_task():
conn.commit()
conn.close()
force_close_before_reset()
+ check_fib_key_monitors()
check_key_monitors()
check_order_monitors()
except:
@@ -4925,7 +5352,8 @@ def render_main_page(page="trade"):
key_gate_rule_text = (
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"自动开仓盈亏比 > {KEY_AUTO_MIN_PLANNED_RR}:1|日成交量排名前 {KEY_DAILY_VOLUME_RANK_MAX}|"
+ f"斐波:添加后立即挂限价 @ E,失效按标记价触达 H/L(未成交撤单)"
)
conn.close()
return render_template(
@@ -5056,7 +5484,9 @@ def api_account_snapshot():
@login_required
def api_price_snapshot():
conn = get_db()
- key_rows = conn.execute("SELECT id,symbol,monitor_type,direction,upper,lower FROM key_monitors").fetchall()
+ key_rows = conn.execute(
+ "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,exchange_symbol,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage FROM order_monitors WHERE status='active'"
).fetchall()
@@ -5093,18 +5523,33 @@ def api_price_snapshot():
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"])
gate = None
- 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
+ if is_fib:
+ direction = (r["direction"] or "long").lower()
+ 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 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')}"
+ 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 = (
@@ -5143,7 +5588,7 @@ def api_price_snapshot():
"lower_diff": lower_diff,
"lower_pct": lower_pct,
"gate_summary": gate_summary,
- "gate_ok": bool(gate and gate.get("ok")),
+ "gate_ok": fib_gate_ok if is_fib else bool(gate and gate.get("ok")),
"gate_metrics": gate_metrics,
})
@@ -5598,9 +6043,13 @@ def add_key():
flash("请选择做多或做空")
return redirect("/key_monitor")
mt = (d.get("type") or "").strip()
- allowed_types = tuple(KEY_MONITOR_AUTO_TYPES) + tuple(KEY_MONITOR_ALERT_ONLY_TYPES)
+ allowed_types = (
+ tuple(KEY_MONITOR_AUTO_TYPES)
+ + tuple(KEY_MONITOR_ALERT_ONLY_TYPES)
+ + tuple(FIB_KEY_MONITOR_TYPES)
+ )
if mt not in allowed_types:
- flash("监控类型无效,请选择:箱体突破、收敛突破、关键阻力位、关键支撑位")
+ flash("监控类型无效")
return redirect("/key_monitor")
rank, total = _daily_volume_rank(symbol)
if rank is None:
@@ -5626,6 +6075,15 @@ def add_key():
pass
upper_px = round_price_to_exchange(ex_sym_key, float(d["upper"]))
lower_px = round_price_to_exchange(ex_sym_key, float(d["lower"]))
+ if is_fib_key_monitor_type(mt):
+ ok_fib, err_fib = _add_fib_key_monitor(conn, symbol, direction_sel, mt, upper_px, lower_px)
+ conn.commit()
+ conn.close()
+ if not ok_fib:
+ flash(err_fib or "斐波监控添加失败")
+ return redirect("/key_monitor")
+ flash(f"斐波监控已添加,限价单已挂出({symbol} 日成交量排名 {rank}/{total})")
+ return redirect("/key_monitor")
conn.execute(
"INSERT INTO key_monitors (symbol,monitor_type,direction,upper,lower) VALUES (?,?,?,?,?)",
(symbol, mt, direction_sel, upper_px, lower_px),
@@ -5996,6 +6454,8 @@ def delete_key_monitor(kid):
if not row:
conn.close()
return jsonify({"ok": False, "error": "not_found"})
+ if is_fib_key_monitor_type(row["monitor_type"]):
+ _cancel_fib_monitor_limit(row)
insert_key_monitor_history(conn, row, int(row["notification_count"] or 0), None, "manual")
cur = conn.execute("DELETE FROM key_monitors WHERE id=?", (kid,))
conn.commit()
@@ -6019,6 +6479,8 @@ def del_key(id):
conn = get_db()
row = conn.execute("SELECT * FROM key_monitors WHERE id=?", (id,)).fetchone()
if row:
+ if is_fib_key_monitor_type(row["monitor_type"]):
+ _cancel_fib_monitor_limit(row)
insert_key_monitor_history(conn, row, int(row["notification_count"] or 0), None, "manual")
conn.execute("DELETE FROM key_monitors WHERE id=?", (id,))
conn.commit()
diff --git a/crypto_monitor_gate/templates/index.html b/crypto_monitor_gate/templates/index.html
index d484473..3e9e16f 100644
--- a/crypto_monitor_gate/templates/index.html
+++ b/crypto_monitor_gate/templates/index.html
@@ -239,6 +239,8 @@
@@ -264,6 +266,7 @@
上沿: {{ k.upper }}
下沿: {{ k.lower }}
+ {% if k.fib_entry_price %}挂E: {{ k.fib_entry_price }}{% endif %}
已提醒: {{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}
diff --git a/fib_key_monitor_lib.py b/fib_key_monitor_lib.py
new file mode 100644
index 0000000..140e1e3
--- /dev/null
+++ b/fib_key_monitor_lib.py
@@ -0,0 +1,60 @@
+"""斐波关键位监控:纯计算与类型判断(Gate / Binance 主站共用)。"""
+
+FIB_KEY_MONITOR_TYPES = frozenset({"斐波回调0.618", "斐波回调0.786"})
+
+FIB_RATIO_BY_TYPE = {
+ "斐波回调0.618": 0.618,
+ "斐波回调0.786": 0.786,
+}
+
+
+def is_fib_key_monitor_type(monitor_type):
+ return (monitor_type or "").strip() in FIB_KEY_MONITOR_TYPES
+
+
+def fib_ratio_from_type(monitor_type):
+ return FIB_RATIO_BY_TYPE.get((monitor_type or "").strip())
+
+
+def calc_fib_plan(direction, upper, lower, ratio):
+ """
+ 上沿 H、下沿 L;挂单价 E = L + ratio*(H-L)。
+ 多:SL=L,TP=H;空:SL=H,TP=L。
+ 返回 (entry, stop_loss, take_profit) 或 None。
+ """
+ try:
+ h = float(upper)
+ l = float(lower)
+ r = float(ratio)
+ except (TypeError, ValueError):
+ return None
+ if h <= l or r <= 0 or r >= 1:
+ return None
+ span = h - l
+ entry = l + r * span
+ direction = (direction or "long").strip().lower()
+ if direction == "short":
+ return entry, h, l
+ return entry, l, h
+
+
+def stored_key_signal_type(monitor_type):
+ """写入 order_monitors / trade_records 的 key_signal_type(箱体/收敛/斐波)。"""
+ mt = (monitor_type or "").strip()
+ if mt in FIB_KEY_MONITOR_TYPES:
+ return mt
+ return None
+
+
+def fib_invalidate_by_mark(direction, mark_price, upper, lower):
+ """先触达止盈侧(标记价)则失效。多:mark>=H;空:mark<=L。"""
+ try:
+ m = float(mark_price)
+ h = float(upper)
+ l = float(lower)
+ except (TypeError, ValueError):
+ return False
+ direction = (direction or "long").strip().lower()
+ if direction == "short":
+ return m <= l
+ return m >= h