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:
root 2026-03-02 10:10:11 +00:00
parent 7e8f83fd5a
commit ab27e5a4da

View File

@ -41,6 +41,7 @@ CHECK_INTERVAL = 30 # 对账间隔(秒)
SL_REHANG_DELAYS = [0, 3] # SL补挂重试延迟(秒)
MAX_REHANG_RETRIES = 2
MISMATCH_ESCALATION_SEC = 60 # 差异持续超过此秒数升级告警
RISK_PER_TRADE_USD = float(os.getenv("RISK_PER_TRADE_USD", "2")) # $2=1R
SYMBOLS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"]
@ -152,7 +153,7 @@ def get_local_positions(conn):
cur = conn.cursor()
cur.execute("""
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
WHERE status IN ('active', 'tp1_hit')
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],
"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],
"binance_order_id": row[11], "entry_ts": row[12],
"binance_order_id": row[11],
})
return positions
@ -413,29 +415,49 @@ async def check_closed_positions(session, conn):
status = "unknown"
exit_price = lp["entry_price"] # fallback
# 尝试从最近交易记录获取
# 尝试从最近交易记录获取成交价和手续费
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:
# 取最近的平仓成交reduceOnly或最后几笔
last_trade = trades_data[-1]
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":
raw_pnl_r = (exit_price - lp["entry_price"]) / rd
gross_pnl_r = (exit_price - lp["entry_price"]) / rd
else:
raw_pnl_r = (lp["entry_price"] - exit_price) / rd
gross_pnl_r = (lp["entry_price"] - exit_price) / rd
if lp["tp1_hit"]:
tp1_r = abs(lp["tp1_price"] - lp["entry_price"]) / rd
pnl_r = 0.5 * tp1_r + 0.5 * raw_pnl_r
else:
pnl_r = raw_pnl_r
gross_pnl_r = 0.5 * tp1_r + 0.5 * gross_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
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:
@ -448,12 +470,103 @@ async def check_closed_positions(session, conn):
status = "closed"
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
""", (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()
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. 检查已平仓
await check_closed_positions(session, conn)
# 4. 资金费率结算追踪
await track_funding_fees(session, conn)
await asyncio.sleep(CHECK_INTERVAL)
except KeyboardInterrupt: