feat: 资金费率结算追踪 + 平仓PnL用真实手续费
资金费率追踪(position_sync.py): - 每8小时结算窗口(UTC 0/8/16后5分钟内)查询币安income API - 按symbol汇总FUNDING_FEE,累加到live_trades.funding_fee_usdt - 只查最近30分钟内的记录,防止重复计入 平仓PnL改进: - 从币安userTrades获取真实commission手续费(不再估算) - PnL拆解: net = gross - fee_r - funding_r - fee_usdt写入DB - 日志输出完整拆解: gross/fee/funding/net
This commit is contained in:
parent
7e8f83fd5a
commit
ab27e5a4da
@ -41,6 +41,7 @@ CHECK_INTERVAL = 30 # 对账间隔(秒)
|
|||||||
SL_REHANG_DELAYS = [0, 3] # SL补挂重试延迟(秒)
|
SL_REHANG_DELAYS = [0, 3] # SL补挂重试延迟(秒)
|
||||||
MAX_REHANG_RETRIES = 2
|
MAX_REHANG_RETRIES = 2
|
||||||
MISMATCH_ESCALATION_SEC = 60 # 差异持续超过此秒数升级告警
|
MISMATCH_ESCALATION_SEC = 60 # 差异持续超过此秒数升级告警
|
||||||
|
RISK_PER_TRADE_USD = float(os.getenv("RISK_PER_TRADE_USD", "2")) # $2=1R
|
||||||
|
|
||||||
SYMBOLS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"]
|
SYMBOLS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"]
|
||||||
|
|
||||||
@ -152,7 +153,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
|
tp1_hit, status, risk_distance, binance_order_id, entry_ts
|
||||||
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
|
||||||
@ -163,6 +164,7 @@ def get_local_positions(conn):
|
|||||||
"id": row[0], "symbol": row[1], "strategy": row[2], "direction": row[3],
|
"id": row[0], "symbol": row[1], "strategy": row[2], "direction": row[3],
|
||||||
"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],
|
"binance_order_id": row[11],
|
||||||
})
|
})
|
||||||
return positions
|
return positions
|
||||||
@ -413,29 +415,49 @@ async def check_closed_positions(session, conn):
|
|||||||
status = "unknown"
|
status = "unknown"
|
||||||
exit_price = lp["entry_price"] # fallback
|
exit_price = lp["entry_price"] # fallback
|
||||||
|
|
||||||
# 尝试从最近交易记录获取
|
# 尝试从最近交易记录获取成交价和手续费
|
||||||
trades_data, trades_status = await binance_request(session, "GET", "/fapi/v1/userTrades", {
|
trades_data, trades_status = await binance_request(session, "GET", "/fapi/v1/userTrades", {
|
||||||
"symbol": symbol, "limit": 5
|
"symbol": symbol, "limit": 20
|
||||||
})
|
})
|
||||||
|
actual_fee_usdt = 0
|
||||||
if trades_status == 200 and isinstance(trades_data, list) and trades_data:
|
if trades_status == 200 and isinstance(trades_data, list) and trades_data:
|
||||||
|
# 取最近的平仓成交(reduceOnly或最后几笔)
|
||||||
last_trade = trades_data[-1]
|
last_trade = trades_data[-1]
|
||||||
exit_price = float(last_trade.get("price", exit_price))
|
exit_price = float(last_trade.get("price", exit_price))
|
||||||
|
# 汇总最近相关成交的手续费(开仓+平仓)
|
||||||
|
entry_ts = lp.get("entry_ts", 0)
|
||||||
|
for t in trades_data:
|
||||||
|
t_time = int(t.get("time", 0))
|
||||||
|
if t_time >= entry_ts - 5000: # 开仓前5秒到现在的所有成交
|
||||||
|
actual_fee_usdt += abs(float(t.get("commission", 0)))
|
||||||
|
|
||||||
# 计算pnl
|
# 计算pnl — gross(不含费)
|
||||||
if lp["direction"] == "LONG":
|
if lp["direction"] == "LONG":
|
||||||
raw_pnl_r = (exit_price - lp["entry_price"]) / rd
|
gross_pnl_r = (exit_price - lp["entry_price"]) / rd
|
||||||
else:
|
else:
|
||||||
raw_pnl_r = (lp["entry_price"] - exit_price) / rd
|
gross_pnl_r = (lp["entry_price"] - exit_price) / rd
|
||||||
|
|
||||||
if lp["tp1_hit"]:
|
if lp["tp1_hit"]:
|
||||||
tp1_r = abs(lp["tp1_price"] - lp["entry_price"]) / rd
|
tp1_r = abs(lp["tp1_price"] - lp["entry_price"]) / rd
|
||||||
pnl_r = 0.5 * tp1_r + 0.5 * raw_pnl_r
|
gross_pnl_r = 0.5 * tp1_r + 0.5 * gross_pnl_r
|
||||||
else:
|
|
||||||
pnl_r = raw_pnl_r
|
|
||||||
|
|
||||||
# 扣手续费
|
# 手续费(R) — 用实际成交手续费
|
||||||
|
if actual_fee_usdt > 0:
|
||||||
|
fee_r = actual_fee_usdt / (RISK_PER_TRADE_USD if RISK_PER_TRADE_USD > 0 else rd)
|
||||||
|
else:
|
||||||
|
# fallback: 按0.1%估算(开+平各0.05%)
|
||||||
fee_r = 0.001 * lp["entry_price"] / rd
|
fee_r = 0.001 * lp["entry_price"] / rd
|
||||||
pnl_r -= fee_r
|
|
||||||
|
# funding费(R)
|
||||||
|
funding_usdt = 0
|
||||||
|
cur.execute("SELECT COALESCE(funding_fee_usdt, 0) FROM live_trades WHERE id = %s", (lp["id"],))
|
||||||
|
fr_row = cur.fetchone()
|
||||||
|
if fr_row:
|
||||||
|
funding_usdt = fr_row[0]
|
||||||
|
funding_r = abs(funding_usdt) / (RISK_PER_TRADE_USD if RISK_PER_TRADE_USD > 0 else rd) if funding_usdt < 0 else 0
|
||||||
|
|
||||||
|
# 净PnL = gross - fee - funding_cost
|
||||||
|
pnl_r = gross_pnl_r - fee_r - funding_r
|
||||||
|
|
||||||
# 判断状态
|
# 判断状态
|
||||||
if pnl_r > 0.5:
|
if pnl_r > 0.5:
|
||||||
@ -448,12 +470,103 @@ async def check_closed_positions(session, conn):
|
|||||||
status = "closed"
|
status = "closed"
|
||||||
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
UPDATE live_trades SET status=%s, exit_price=%s, exit_ts=%s, pnl_r=%s
|
UPDATE live_trades SET status=%s, exit_price=%s, exit_ts=%s, pnl_r=%s, fee_usdt=%s
|
||||||
WHERE id=%s
|
WHERE id=%s
|
||||||
""", (status, exit_price, now_ms, round(pnl_r, 4), lp["id"]))
|
""", (status, exit_price, now_ms, round(pnl_r, 4), round(actual_fee_usdt, 4), lp["id"]))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
logger.info(f"[{symbol}] 📝 平仓记录: {status} | exit={exit_price:.4f} | pnl={pnl_r:+.2f}R")
|
logger.info(
|
||||||
|
f"[{symbol}] 📝 平仓: {status} | exit={exit_price:.4f} | "
|
||||||
|
f"gross={gross_pnl_r:+.3f}R fee={fee_r:.3f}R({actual_fee_usdt:.4f}$) "
|
||||||
|
f"funding={funding_usdt:+.4f}$ | net={pnl_r:+.3f}R"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============ 资金费率结算追踪 ============
|
||||||
|
|
||||||
|
# 币安结算时间:UTC 00:00, 08:00, 16:00
|
||||||
|
FUNDING_SETTLEMENT_HOURS = [0, 8, 16]
|
||||||
|
_last_funding_check_ts = 0 # 上次查funding的时间戳
|
||||||
|
|
||||||
|
|
||||||
|
async def track_funding_fees(session, conn):
|
||||||
|
"""
|
||||||
|
查询币安资金费率收支,更新到live_trades的funding_fee_usdt字段。
|
||||||
|
只在结算时间点附近查询(每8小时一次,±5分钟窗口内查一次)。
|
||||||
|
"""
|
||||||
|
global _last_funding_check_ts
|
||||||
|
import datetime as _dt
|
||||||
|
|
||||||
|
now = _dt.datetime.now(_dt.timezone.utc)
|
||||||
|
now_ts = time.time()
|
||||||
|
|
||||||
|
# 判断是否在结算窗口内(结算时间后0-5分钟)
|
||||||
|
in_settlement_window = False
|
||||||
|
for h in FUNDING_SETTLEMENT_HOURS:
|
||||||
|
settlement_time = now.replace(hour=h, minute=0, second=0, microsecond=0)
|
||||||
|
diff_sec = (now - settlement_time).total_seconds()
|
||||||
|
if 0 <= diff_sec <= 300: # 结算后5分钟内
|
||||||
|
in_settlement_window = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# 不在窗口内,或者5分钟内已经查过了
|
||||||
|
if not in_settlement_window:
|
||||||
|
return
|
||||||
|
if now_ts - _last_funding_check_ts < 300:
|
||||||
|
return
|
||||||
|
|
||||||
|
_last_funding_check_ts = now_ts
|
||||||
|
logger.info("💰 资金费率结算窗口,查询funding收支...")
|
||||||
|
|
||||||
|
# 获取当前活跃持仓
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id, symbol, direction, entry_ts, COALESCE(funding_fee_usdt, 0) as current_funding
|
||||||
|
FROM live_trades WHERE status IN ('active', 'tp1_hit')
|
||||||
|
""")
|
||||||
|
active = cur.fetchall()
|
||||||
|
|
||||||
|
if not active:
|
||||||
|
logger.info("💰 无活跃持仓,跳过funding查询")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 查币安最近的funding收入记录
|
||||||
|
# GET /fapi/v1/income?incomeType=FUNDING_FEE&limit=100
|
||||||
|
start_ts = int((now_ts - 3600) * 1000) # 最近1小时
|
||||||
|
data, status = await binance_request(session, "GET", "/fapi/v1/income", {
|
||||||
|
"incomeType": "FUNDING_FEE",
|
||||||
|
"startTime": start_ts,
|
||||||
|
"limit": 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
if status != 200 or not isinstance(data, list):
|
||||||
|
logger.warning(f"💰 查询funding income失败: {status}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 按symbol汇总本次结算的funding
|
||||||
|
funding_by_symbol = {}
|
||||||
|
for item in data:
|
||||||
|
sym = item.get("symbol", "")
|
||||||
|
income = float(item.get("income", 0))
|
||||||
|
ts = int(item.get("time", 0))
|
||||||
|
# 只取最近30分钟内的(本次结算的)
|
||||||
|
if now_ts * 1000 - ts < 1800000:
|
||||||
|
funding_by_symbol[sym] = funding_by_symbol.get(sym, 0) + income
|
||||||
|
|
||||||
|
if not funding_by_symbol:
|
||||||
|
logger.info("💰 本次结算无funding记录")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 更新到live_trades
|
||||||
|
for trade_id, symbol, direction, entry_ts, current_funding in active:
|
||||||
|
fr_income = funding_by_symbol.get(symbol, 0)
|
||||||
|
if fr_income != 0:
|
||||||
|
new_total = current_funding + fr_income
|
||||||
|
cur.execute("UPDATE live_trades SET funding_fee_usdt = %s WHERE id = %s", (new_total, trade_id))
|
||||||
|
logger.info(f"[{symbol}] 💰 Funding: {fr_income:+.4f} USDT (累计: {new_total:+.4f})")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
logger.info(f"💰 Funding更新完成: {funding_by_symbol}")
|
||||||
|
|
||||||
|
|
||||||
# ============ 主循环 ============
|
# ============ 主循环 ============
|
||||||
@ -478,6 +591,9 @@ async def main():
|
|||||||
# 3. 检查已平仓
|
# 3. 检查已平仓
|
||||||
await check_closed_positions(session, conn)
|
await check_closed_positions(session, conn)
|
||||||
|
|
||||||
|
# 4. 资金费率结算追踪
|
||||||
|
await track_funding_fees(session, conn)
|
||||||
|
|
||||||
await asyncio.sleep(CHECK_INTERVAL)
|
await asyncio.sleep(CHECK_INTERVAL)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user