fix: P0审阅修复 + P1/P2增强
P0-1: SL挂单失败→重试2次→3次失败紧急市价平仓+写event P0-2: TP1检测改用DB qty字段(新增)比对仓位减少,不再用orderId P0-3: emergency-close/block-new/resume/config PUT加admin权限验证 P0-5: risk_guard全平qty按币种精度格式化(BTC:3/ETH:3/XRP:0/SOL:2) P1-3: NOTIFY收到后立即处理跳过sleep,减少信号延迟 P2-1: 三个进程加DB连接断线重连(ensure_db_conn) DB: live_trades新增qty字段
This commit is contained in:
parent
855df24eba
commit
638589852b
@ -411,16 +411,24 @@ async def execute_entry(session: aiohttp.ClientSession, signal: dict, db_conn):
|
|||||||
half_qty = round(qty / 2, prec["qty"])
|
half_qty = round(qty / 2, prec["qty"])
|
||||||
other_half = round(qty - half_qty, prec["qty"])
|
other_half = round(qty - half_qty, prec["qty"])
|
||||||
|
|
||||||
# SL - 全仓
|
# SL - 全仓(失败重试2次,3次都失败则紧急平仓)
|
||||||
t_before_sl = time.time() * 1000
|
t_before_sl = time.time() * 1000
|
||||||
sl_data, sl_status = await place_stop_order(session, symbol, close_side, sl_price, qty, "STOP_MARKET")
|
sl_data, sl_status = await place_stop_order(session, symbol, close_side, sl_price, qty, "STOP_MARKET")
|
||||||
|
if sl_status != 200:
|
||||||
|
for retry in range(2):
|
||||||
|
logger.warning(f"[{symbol}] SL挂单重试 {retry+1}/2...")
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
sl_data, sl_status = await place_stop_order(session, symbol, close_side, sl_price, qty, "STOP_MARKET")
|
||||||
|
if sl_status == 200:
|
||||||
|
break
|
||||||
|
if sl_status != 200:
|
||||||
|
logger.error(f"[{symbol}] ❌ SL 3次全部失败,紧急市价平仓! data={sl_data}")
|
||||||
|
await place_market_order(session, symbol, close_side, qty)
|
||||||
|
_log_event(db_conn, "critical", "trade", f"SL挂单3次失败,已紧急平仓", symbol, {"sl_data": str(sl_data)})
|
||||||
|
return None
|
||||||
t_after_sl = time.time() * 1000
|
t_after_sl = time.time() * 1000
|
||||||
protection_gap_ms = int(t_after_sl - t_fill)
|
protection_gap_ms = int(t_after_sl - t_fill)
|
||||||
|
|
||||||
if sl_status != 200:
|
|
||||||
logger.error(f"[{symbol}] ⚠️ SL挂单失败! 裸奔中! data={sl_data}")
|
|
||||||
# TODO: 自动补挂逻辑
|
|
||||||
|
|
||||||
# TP1 - 半仓
|
# TP1 - 半仓
|
||||||
tp1_type = "TAKE_PROFIT_MARKET"
|
tp1_type = "TAKE_PROFIT_MARKET"
|
||||||
await place_stop_order(session, symbol, close_side, tp1_price, half_qty, tp1_type)
|
await place_stop_order(session, symbol, close_side, tp1_price, half_qty, tp1_type)
|
||||||
@ -437,13 +445,15 @@ async def execute_entry(session: aiohttp.ClientSession, signal: dict, db_conn):
|
|||||||
sl_price, tp1_price, tp2_price, score, tier, status,
|
sl_price, tp1_price, tp2_price, score, tier, status,
|
||||||
risk_distance, atr_at_entry, score_factors, signal_id,
|
risk_distance, atr_at_entry, score_factors, signal_id,
|
||||||
binance_order_id, fill_price, slippage_bps,
|
binance_order_id, fill_price, slippage_bps,
|
||||||
protection_gap_ms, signal_to_order_ms, order_to_fill_ms
|
protection_gap_ms, signal_to_order_ms, order_to_fill_ms,
|
||||||
|
qty
|
||||||
) VALUES (
|
) VALUES (
|
||||||
%s, %s, %s, %s, %s,
|
%s, %s, %s, %s, %s,
|
||||||
%s, %s, %s, %s, %s, 'active',
|
%s, %s, %s, %s, %s, 'active',
|
||||||
%s, %s, %s, %s,
|
%s, %s, %s, %s,
|
||||||
%s, %s, %s,
|
%s, %s, %s,
|
||||||
%s, %s, %s
|
%s, %s, %s,
|
||||||
|
%s
|
||||||
) RETURNING id
|
) RETURNING id
|
||||||
""", (
|
""", (
|
||||||
symbol, strategy, direction, fill_price, int(t_fill),
|
symbol, strategy, direction, fill_price, int(t_fill),
|
||||||
@ -451,6 +461,7 @@ async def execute_entry(session: aiohttp.ClientSession, signal: dict, db_conn):
|
|||||||
risk_distance, atr, json.dumps(factors) if factors else None, signal_id,
|
risk_distance, atr, json.dumps(factors) if factors else None, signal_id,
|
||||||
order_id, fill_price, round(slippage_bps, 2),
|
order_id, fill_price, round(slippage_bps, 2),
|
||||||
protection_gap_ms, signal_to_order_ms, order_to_fill_ms,
|
protection_gap_ms, signal_to_order_ms, order_to_fill_ms,
|
||||||
|
qty,
|
||||||
))
|
))
|
||||||
trade_id = cur.fetchone()[0]
|
trade_id = cur.fetchone()[0]
|
||||||
db_conn.commit()
|
db_conn.commit()
|
||||||
@ -608,14 +619,30 @@ async def main():
|
|||||||
# 工作DB连接
|
# 工作DB连接
|
||||||
work_conn = psycopg2.connect(**DB_CONFIG)
|
work_conn = psycopg2.connect(**DB_CONFIG)
|
||||||
|
|
||||||
|
def ensure_db_conn(conn):
|
||||||
|
try:
|
||||||
|
conn.cursor().execute("SELECT 1")
|
||||||
|
return conn
|
||||||
|
except Exception:
|
||||||
|
logger.warning("⚠️ DB连接断开,重连中...")
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return psycopg2.connect(**DB_CONFIG)
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as http_session:
|
async with aiohttp.ClientSession() as http_session:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
# 检查PG NOTIFY(非阻塞,超时1秒)
|
# 检查PG NOTIFY(非阻塞)
|
||||||
|
got_notify = False
|
||||||
if listen_conn.poll() == psycopg2.extensions.POLL_OK:
|
if listen_conn.poll() == psycopg2.extensions.POLL_OK:
|
||||||
while listen_conn.notifies:
|
while listen_conn.notifies:
|
||||||
notify = listen_conn.notifies.pop(0)
|
notify = listen_conn.notifies.pop(0)
|
||||||
logger.info(f"📡 收到NOTIFY: {notify.payload}")
|
logger.info(f"📡 收到NOTIFY: {notify.payload}")
|
||||||
|
got_notify = True
|
||||||
|
|
||||||
|
work_conn = ensure_db_conn(work_conn)
|
||||||
|
|
||||||
# 获取待处理信号(NOTIFY + 轮询双保险)
|
# 获取待处理信号(NOTIFY + 轮询双保险)
|
||||||
signals = fetch_pending_signals(work_conn)
|
signals = fetch_pending_signals(work_conn)
|
||||||
@ -632,7 +659,8 @@ async def main():
|
|||||||
if trade_id:
|
if trade_id:
|
||||||
logger.info(f"[{sig['symbol']}] ✅ trade_id={trade_id} 开仓成功")
|
logger.info(f"[{sig['symbol']}] ✅ trade_id={trade_id} 开仓成功")
|
||||||
|
|
||||||
await asyncio.sleep(1) # 1秒轮询作为fallback
|
if not got_notify:
|
||||||
|
await asyncio.sleep(1) # 无NOTIFY时才sleep,有NOTIFY立即处理下一轮
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info("🛑 收到退出信号")
|
logger.info("🛑 收到退出信号")
|
||||||
|
|||||||
@ -1471,9 +1471,16 @@ async def live_risk_status(user: dict = Depends(get_current_user)):
|
|||||||
return {"status": "unknown", "error": "risk_guard_state.json not found"}
|
return {"status": "unknown", "error": "risk_guard_state.json not found"}
|
||||||
|
|
||||||
|
|
||||||
|
def _require_admin(user: dict):
|
||||||
|
"""检查管理员权限"""
|
||||||
|
if user.get("role") != "admin":
|
||||||
|
raise HTTPException(status_code=403, detail="仅管理员可执行此操作")
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/live/emergency-close")
|
@app.post("/api/live/emergency-close")
|
||||||
async def live_emergency_close(user: dict = Depends(get_current_user)):
|
async def live_emergency_close(user: dict = Depends(get_current_user)):
|
||||||
"""紧急全平(写标记文件,由risk_guard执行)"""
|
"""紧急全平(写标记文件,由risk_guard执行)"""
|
||||||
|
_require_admin(user)
|
||||||
try:
|
try:
|
||||||
import json as _json
|
import json as _json
|
||||||
with open("/tmp/risk_guard_emergency.json", "w") as f:
|
with open("/tmp/risk_guard_emergency.json", "w") as f:
|
||||||
@ -1486,6 +1493,7 @@ async def live_emergency_close(user: dict = Depends(get_current_user)):
|
|||||||
@app.post("/api/live/block-new")
|
@app.post("/api/live/block-new")
|
||||||
async def live_block_new(user: dict = Depends(get_current_user)):
|
async def live_block_new(user: dict = Depends(get_current_user)):
|
||||||
"""禁止新开仓"""
|
"""禁止新开仓"""
|
||||||
|
_require_admin(user)
|
||||||
try:
|
try:
|
||||||
import json as _json
|
import json as _json
|
||||||
with open("/tmp/risk_guard_emergency.json", "w") as f:
|
with open("/tmp/risk_guard_emergency.json", "w") as f:
|
||||||
@ -1498,6 +1506,7 @@ async def live_block_new(user: dict = Depends(get_current_user)):
|
|||||||
@app.post("/api/live/resume")
|
@app.post("/api/live/resume")
|
||||||
async def live_resume(user: dict = Depends(get_current_user)):
|
async def live_resume(user: dict = Depends(get_current_user)):
|
||||||
"""恢复交易"""
|
"""恢复交易"""
|
||||||
|
_require_admin(user)
|
||||||
try:
|
try:
|
||||||
import json as _json
|
import json as _json
|
||||||
with open("/tmp/risk_guard_emergency.json", "w") as f:
|
with open("/tmp/risk_guard_emergency.json", "w") as f:
|
||||||
@ -1904,6 +1913,7 @@ async def live_config_get(user: dict = Depends(get_current_user)):
|
|||||||
@app.put("/api/live/config")
|
@app.put("/api/live/config")
|
||||||
async def live_config_update(request: Request, user: dict = Depends(get_current_user)):
|
async def live_config_update(request: Request, user: dict = Depends(get_current_user)):
|
||||||
"""更新实盘配置"""
|
"""更新实盘配置"""
|
||||||
|
_require_admin(user)
|
||||||
body = await request.json()
|
body = await request.json()
|
||||||
updated = []
|
updated = []
|
||||||
for key, value in body.items():
|
for key, value in body.items():
|
||||||
|
|||||||
@ -158,7 +158,7 @@ def get_local_positions(conn):
|
|||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT id, symbol, strategy, direction, entry_price, sl_price, tp1_price, tp2_price,
|
SELECT id, symbol, strategy, direction, entry_price, sl_price, tp1_price, tp2_price,
|
||||||
tp1_hit, status, risk_distance, binance_order_id, entry_ts
|
tp1_hit, status, risk_distance, binance_order_id, entry_ts, qty
|
||||||
FROM live_trades
|
FROM live_trades
|
||||||
WHERE status IN ('active', 'tp1_hit')
|
WHERE status IN ('active', 'tp1_hit')
|
||||||
ORDER BY entry_ts DESC
|
ORDER BY entry_ts DESC
|
||||||
@ -170,7 +170,7 @@ def get_local_positions(conn):
|
|||||||
"entry_price": row[4], "sl_price": row[5], "tp1_price": row[6], "tp2_price": row[7],
|
"entry_price": row[4], "sl_price": row[5], "tp1_price": row[6], "tp2_price": row[7],
|
||||||
"tp1_hit": row[8], "status": row[9], "risk_distance": row[10],
|
"tp1_hit": row[8], "status": row[9], "risk_distance": row[10],
|
||||||
"binance_order_id": row[11], "entry_ts": row[12],
|
"binance_order_id": row[11], "entry_ts": row[12],
|
||||||
"binance_order_id": row[11],
|
"qty": row[13],
|
||||||
})
|
})
|
||||||
return positions
|
return positions
|
||||||
|
|
||||||
@ -359,7 +359,8 @@ async def check_tp1_triggers(session, conn):
|
|||||||
if not tp1_found and lp["status"] == "active":
|
if not tp1_found and lp["status"] == "active":
|
||||||
# TP1可能已触发,验证仓位是否减半
|
# TP1可能已触发,验证仓位是否减半
|
||||||
bp = (await get_binance_positions(session)).get(symbol)
|
bp = (await get_binance_positions(session)).get(symbol)
|
||||||
if bp and abs(bp["amount"]) < abs(float(lp.get("binance_order_id", "0") or "0")):
|
entry_qty = lp.get("qty") or 0
|
||||||
|
if bp and entry_qty > 0 and abs(bp["amount"]) < entry_qty * 0.75:
|
||||||
# 确认TP1触发
|
# 确认TP1触发
|
||||||
logger.info(f"[{symbol}] ✅ TP1触发! 移SL到保本价")
|
logger.info(f"[{symbol}] ✅ TP1触发! 移SL到保本价")
|
||||||
|
|
||||||
@ -595,6 +596,20 @@ def _log_event(conn, level, category, message, symbol=None, detail=None):
|
|||||||
|
|
||||||
# ============ 主循环 ============
|
# ============ 主循环 ============
|
||||||
|
|
||||||
|
def ensure_db_conn(conn):
|
||||||
|
"""检查DB连接,断线则重连"""
|
||||||
|
try:
|
||||||
|
conn.cursor().execute("SELECT 1")
|
||||||
|
return conn
|
||||||
|
except Exception:
|
||||||
|
logger.warning("⚠️ DB连接断开,重连中...")
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return psycopg2.connect(**DB_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
logger.info("=" * 60)
|
logger.info("=" * 60)
|
||||||
logger.info(f"🔄 Position Sync 启动 | 环境={TRADE_ENV} | 间隔={CHECK_INTERVAL}秒")
|
logger.info(f"🔄 Position Sync 启动 | 环境={TRADE_ENV} | 间隔={CHECK_INTERVAL}秒")
|
||||||
@ -606,6 +621,7 @@ async def main():
|
|||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
conn = ensure_db_conn(conn)
|
||||||
# 1. 对账
|
# 1. 对账
|
||||||
result = await reconcile(session, conn)
|
result = await reconcile(session, conn)
|
||||||
|
|
||||||
|
|||||||
@ -68,6 +68,9 @@ ACCOUNT_UPDATE_STALE_SEC = 20
|
|||||||
CHECK_INTERVAL = 5 # 风控检查间隔(秒)
|
CHECK_INTERVAL = 5 # 风控检查间隔(秒)
|
||||||
|
|
||||||
SYMBOLS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"]
|
SYMBOLS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"]
|
||||||
|
SYMBOL_QTY_PRECISION = {
|
||||||
|
"BTCUSDT": 3, "ETHUSDT": 3, "XRPUSDT": 0, "SOLUSDT": 2,
|
||||||
|
}
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
@ -336,11 +339,13 @@ async def trigger_circuit_break(session, conn, reason: str, action: str = "block
|
|||||||
amt = float(p.get("positionAmt", 0))
|
amt = float(p.get("positionAmt", 0))
|
||||||
if amt != 0:
|
if amt != 0:
|
||||||
close_side = "SELL" if amt > 0 else "BUY"
|
close_side = "SELL" if amt > 0 else "BUY"
|
||||||
|
qty_prec = SYMBOL_QTY_PRECISION.get(symbol, 3)
|
||||||
|
qty_str = f"{abs(amt):.{qty_prec}f}"
|
||||||
await binance_request(session, "POST", "/fapi/v1/order", {
|
await binance_request(session, "POST", "/fapi/v1/order", {
|
||||||
"symbol": symbol, "side": close_side, "type": "MARKET",
|
"symbol": symbol, "side": close_side, "type": "MARKET",
|
||||||
"quantity": str(abs(amt)), "reduceOnly": "true",
|
"quantity": qty_str, "reduceOnly": "true",
|
||||||
})
|
})
|
||||||
logger.info(f"[{symbol}] 🔴 紧急平仓 {close_side} qty={abs(amt)}")
|
logger.info(f"[{symbol}] 🔴 紧急平仓 {close_side} qty={qty_str}")
|
||||||
|
|
||||||
# 记录历史
|
# 记录历史
|
||||||
risk_state.breaker_history.append({
|
risk_state.breaker_history.append({
|
||||||
@ -458,6 +463,20 @@ async def check_emergency_commands(session, conn):
|
|||||||
|
|
||||||
# ============ 主循环 ============
|
# ============ 主循环 ============
|
||||||
|
|
||||||
|
def ensure_db_conn(conn):
|
||||||
|
"""检查DB连接,断线则重连"""
|
||||||
|
try:
|
||||||
|
conn.cursor().execute("SELECT 1")
|
||||||
|
return conn
|
||||||
|
except Exception:
|
||||||
|
logger.warning("⚠️ DB连接断开,重连中...")
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return psycopg2.connect(**DB_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
logger.info("=" * 60)
|
logger.info("=" * 60)
|
||||||
logger.info(f"🛡 Risk Guard 启动 | 环境={TRADE_ENV}")
|
logger.info(f"🛡 Risk Guard 启动 | 环境={TRADE_ENV}")
|
||||||
@ -474,6 +493,7 @@ async def main():
|
|||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
conn = ensure_db_conn(conn)
|
||||||
# 0. 检查自动恢复
|
# 0. 检查自动恢复
|
||||||
check_auto_resume()
|
check_auto_resume()
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user