增加斐波那契下单
This commit is contained in:
+478
-12
@@ -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()
|
||||
|
||||
@@ -239,6 +239,8 @@
|
||||
<select name="type" required>
|
||||
<option value="箱体突破">箱体突破</option>
|
||||
<option value="收敛突破">收敛突破</option>
|
||||
<option value="斐波回调0.618">斐波回调0.618</option>
|
||||
<option value="斐波回调0.786">斐波回调0.786</option>
|
||||
<option value="关键阻力位">关键阻力位</option>
|
||||
<option value="关键支撑位">关键支撑位</option>
|
||||
</select>
|
||||
@@ -264,6 +266,7 @@
|
||||
<div class="pos-meta">
|
||||
<span class="pos-meta-item">上沿: {{ k.upper }}</span>
|
||||
<span class="pos-meta-item">下沿: {{ k.lower }}</span>
|
||||
{% if k.fib_entry_price %}<span class="pos-meta-item">挂E: {{ k.fib_entry_price }}</span>{% endif %}
|
||||
<span class="pos-meta-item">已提醒: {{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}</span>
|
||||
</div>
|
||||
<div class="pos-grid">
|
||||
|
||||
+474
-12
@@ -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()
|
||||
|
||||
@@ -239,6 +239,8 @@
|
||||
<select name="type" required>
|
||||
<option value="箱体突破">箱体突破</option>
|
||||
<option value="收敛突破">收敛突破</option>
|
||||
<option value="斐波回调0.618">斐波回调0.618</option>
|
||||
<option value="斐波回调0.786">斐波回调0.786</option>
|
||||
<option value="关键阻力位">关键阻力位</option>
|
||||
<option value="关键支撑位">关键支撑位</option>
|
||||
</select>
|
||||
@@ -264,6 +266,7 @@
|
||||
<div class="pos-meta">
|
||||
<span class="pos-meta-item">上沿: {{ k.upper }}</span>
|
||||
<span class="pos-meta-item">下沿: {{ k.lower }}</span>
|
||||
{% if k.fib_entry_price %}<span class="pos-meta-item">挂E: {{ k.fib_entry_price }}</span>{% endif %}
|
||||
<span class="pos-meta-item">已提醒: {{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}</span>
|
||||
</div>
|
||||
<div class="pos-grid">
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user