修复币安交易记录
This commit is contained in:
@@ -75,6 +75,8 @@ BINANCE_TRIGGER_WORKING_TYPE=CONTRACT_PRICE
|
|||||||
# EXCHANGE_DISPLAY_NAME=Binance
|
# EXCHANGE_DISPLAY_NAME=Binance
|
||||||
# 企业微信推送里展示的账户备注
|
# 企业微信推送里展示的账户备注
|
||||||
# BINANCE_ACCOUNT_LABEL=binance实盘账户
|
# BINANCE_ACCOUNT_LABEL=binance实盘账户
|
||||||
|
# 盈亏同步口径:false=与 App「仓位历史-实现盈亏」一致(不含资金费);true=含资金费等完整流水
|
||||||
|
# BINANCE_PNL_INCLUDE_FUNDING=false
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# 关键位门控(页面「关键位监控」规则条与 _key_hard_checks 共用)
|
# 关键位门控(页面「关键位监控」规则条与 _key_hard_checks 共用)
|
||||||
|
|||||||
@@ -162,9 +162,16 @@ ORDER_MONITOR_TYPE_KEY_AUTO = "关键位监控"
|
|||||||
KEY_MONITOR_AUTO_TYPES = frozenset({"箱体突破", "收敛突破"})
|
KEY_MONITOR_AUTO_TYPES = frozenset({"箱体突破", "收敛突破"})
|
||||||
EXCHANGE_POSITION_SYNC_FROM_BJ = (os.getenv("EXCHANGE_POSITION_SYNC_FROM_BJ") or "").strip()
|
EXCHANGE_POSITION_SYNC_FROM_BJ = (os.getenv("EXCHANGE_POSITION_SYNC_FROM_BJ") or "").strip()
|
||||||
EXCHANGE_POSITION_HISTORY_LIMIT = max(50, min(1000, int(os.getenv("EXCHANGE_POSITION_HISTORY_LIMIT", "200"))))
|
EXCHANGE_POSITION_HISTORY_LIMIT = max(50, min(1000, int(os.getenv("EXCHANGE_POSITION_HISTORY_LIMIT", "200"))))
|
||||||
|
# 与币安 App「仓位历史-实现盈亏」对齐:仅已实现盈亏 + 手续费(不含资金费)
|
||||||
|
BINANCE_APP_PNL_INCOME_TYPES = frozenset({"REALIZED_PNL", "COMMISSION"})
|
||||||
BINANCE_NET_INCOME_TYPES = frozenset(
|
BINANCE_NET_INCOME_TYPES = frozenset(
|
||||||
{"REALIZED_PNL", "COMMISSION", "FUNDING_FEE", "INSURANCE_CLEAR", "INTERNAL_AUTO_CLOSE"}
|
{"REALIZED_PNL", "COMMISSION", "FUNDING_FEE", "INSURANCE_CLEAR", "INTERNAL_AUTO_CLOSE"}
|
||||||
)
|
)
|
||||||
|
BINANCE_PNL_INCLUDE_FUNDING = os.getenv("BINANCE_PNL_INCLUDE_FUNDING", "false").lower() in (
|
||||||
|
"1",
|
||||||
|
"true",
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
_LAST_EXCHANGE_PNL_SYNC_AT = 0.0
|
_LAST_EXCHANGE_PNL_SYNC_AT = 0.0
|
||||||
KEY_MONITOR_ALERT_ONLY_TYPES = frozenset({"关键阻力位", "关键支撑位"})
|
KEY_MONITOR_ALERT_ONLY_TYPES = frozenset({"关键阻力位", "关键支撑位"})
|
||||||
AUTO_TRANSFER_ENABLED = os.getenv("AUTO_TRANSFER_ENABLED", "false").lower() == "true"
|
AUTO_TRANSFER_ENABLED = os.getenv("AUTO_TRANSFER_ENABLED", "false").lower() == "true"
|
||||||
@@ -2086,6 +2093,48 @@ def _income_entry_trade_id(entry):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def calc_binance_realized_pnl_from_trades(trades):
|
||||||
|
"""
|
||||||
|
按 Binance 成交回报汇总:sum(realizedPnl) + 手续费(与 App「仓位历史-实现盈亏」一致)。
|
||||||
|
"""
|
||||||
|
if not trades:
|
||||||
|
return None
|
||||||
|
total = 0.0
|
||||||
|
has = False
|
||||||
|
for t in trades:
|
||||||
|
info = t.get("info") if isinstance(t.get("info"), dict) else {}
|
||||||
|
for src in (info, t):
|
||||||
|
if not isinstance(src, dict):
|
||||||
|
continue
|
||||||
|
for key in ("realizedPnl", "realized_pnl"):
|
||||||
|
v = src.get(key)
|
||||||
|
if v is None or str(v).strip() == "":
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
total += float(v)
|
||||||
|
has = True
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
fee = t.get("fee")
|
||||||
|
if isinstance(fee, dict) and fee.get("cost") is not None:
|
||||||
|
try:
|
||||||
|
total += float(fee["cost"])
|
||||||
|
has = True
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
comm = info.get("commission")
|
||||||
|
if comm is not None and str(comm).strip() != "":
|
||||||
|
try:
|
||||||
|
c = float(comm)
|
||||||
|
total -= c if c > 0 else -abs(c)
|
||||||
|
has = True
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
if not has:
|
||||||
|
return None
|
||||||
|
return round(total, FUNDS_DECIMALS)
|
||||||
|
|
||||||
|
|
||||||
def calc_pnl_from_closing_trades(direction, entry_price, trades, exchange_symbol=None):
|
def calc_pnl_from_closing_trades(direction, entry_price, trades, exchange_symbol=None):
|
||||||
"""按减仓成交数量×价差汇总盈亏(不含资金费;比单点标记价更接近交易所)。"""
|
"""按减仓成交数量×价差汇总盈亏(不含资金费;比单点标记价更接近交易所)。"""
|
||||||
try:
|
try:
|
||||||
@@ -2173,6 +2222,9 @@ def resolve_trade_pnl_amount(
|
|||||||
if net is not None:
|
if net is not None:
|
||||||
return net, exit_price, eo, ec, sync_key
|
return net, exit_price, eo, ec, sync_key
|
||||||
if closing_trades:
|
if closing_trades:
|
||||||
|
trade_pnl = calc_binance_realized_pnl_from_trades(closing_trades)
|
||||||
|
if trade_pnl is not None:
|
||||||
|
return trade_pnl, exit_price, None, None, None
|
||||||
fill_pnl = calc_pnl_from_closing_trades(direction, entry_price, closing_trades, ex_sym)
|
fill_pnl = calc_pnl_from_closing_trades(direction, entry_price, closing_trades, ex_sym)
|
||||||
if fill_pnl is not None:
|
if fill_pnl is not None:
|
||||||
return fill_pnl, exit_price, None, None, None
|
return fill_pnl, exit_price, None, None, None
|
||||||
@@ -5430,24 +5482,48 @@ def fetch_binance_net_pnl_for_trade(
|
|||||||
):
|
):
|
||||||
if open_ms is None or close_ms is None or close_ms < open_ms:
|
if open_ms is None or close_ms is None or close_ms < open_ms:
|
||||||
return None, None, None, None
|
return None, None, None, None
|
||||||
buffer_ms = 5 * 60 * 1000
|
trade_ids = _trade_ids_from_fills(closing_trades) if closing_trades else None
|
||||||
|
if closing_trades:
|
||||||
|
trade_pnl = calc_binance_realized_pnl_from_trades(closing_trades)
|
||||||
|
if trade_pnl is not None:
|
||||||
|
first_t = None
|
||||||
|
last_t = None
|
||||||
|
for t in closing_trades:
|
||||||
|
ts = _coerce_ts_ms(t.get("timestamp"))
|
||||||
|
if ts:
|
||||||
|
first_t = ts if first_t is None else min(first_t, ts)
|
||||||
|
last_t = ts if last_t is None else max(last_t, ts)
|
||||||
|
ensure_markets_loaded()
|
||||||
|
market = exchange.market(exchange_symbol)
|
||||||
|
cid = market.get("id") or exchange_symbol
|
||||||
|
sync_key = f"trades|{cid}|{direction}|{open_ms}|{close_ms}|{trade_pnl}"
|
||||||
|
eo = ms_to_app_local_str(first_t) if first_t else None
|
||||||
|
ec = ms_to_app_local_str(last_t) if last_t else None
|
||||||
|
return trade_pnl, sync_key, eo, ec
|
||||||
|
income_types = (
|
||||||
|
BINANCE_NET_INCOME_TYPES
|
||||||
|
if BINANCE_PNL_INCLUDE_FUNDING
|
||||||
|
else BINANCE_APP_PNL_INCOME_TYPES
|
||||||
|
)
|
||||||
|
buffer_ms = 90 * 1000 if trade_ids else 5 * 60 * 1000
|
||||||
entries = _fetch_binance_income_entries(
|
entries = _fetch_binance_income_entries(
|
||||||
exchange_symbol, max(0, int(open_ms) - buffer_ms), int(close_ms) + buffer_ms
|
exchange_symbol, max(0, int(open_ms) - buffer_ms), int(close_ms) + buffer_ms
|
||||||
)
|
)
|
||||||
if not entries:
|
if not entries:
|
||||||
return None, None, None, None
|
return None, None, None, None
|
||||||
trade_ids = _trade_ids_from_fills(closing_trades) if closing_trades else None
|
|
||||||
net = 0.0
|
net = 0.0
|
||||||
first_t = None
|
first_t = None
|
||||||
last_t = None
|
last_t = None
|
||||||
for e in entries:
|
for e in entries:
|
||||||
it = (e.get("incomeType") or e.get("income_type") or "").strip()
|
it = (e.get("incomeType") or e.get("income_type") or "").strip()
|
||||||
if it not in BINANCE_NET_INCOME_TYPES:
|
if it not in income_types:
|
||||||
continue
|
continue
|
||||||
if trade_ids and it in ("REALIZED_PNL", "COMMISSION"):
|
if trade_ids and it in ("REALIZED_PNL", "COMMISSION"):
|
||||||
tid = _income_entry_trade_id(e)
|
tid = _income_entry_trade_id(e)
|
||||||
if tid and tid not in trade_ids:
|
if tid and tid not in trade_ids:
|
||||||
continue
|
continue
|
||||||
|
if trade_ids and it == "FUNDING_FEE":
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
net += float(e.get("income") or 0)
|
net += float(e.get("income") or 0)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
@@ -5509,9 +5585,11 @@ def sync_trade_record_exchange_pnl(conn, record_id, commit=True, force=False):
|
|||||||
if net is not None and sync_key:
|
if net is not None and sync_key:
|
||||||
break
|
break
|
||||||
if net is None:
|
if net is None:
|
||||||
net = calc_pnl_from_closing_trades(
|
net = calc_binance_realized_pnl_from_trades(closing_trades)
|
||||||
direction, tr["trigger_price"], closing_trades, ex_sym
|
if net is None:
|
||||||
)
|
net = calc_pnl_from_closing_trades(
|
||||||
|
direction, tr["trigger_price"], closing_trades, ex_sym
|
||||||
|
)
|
||||||
if net is not None:
|
if net is not None:
|
||||||
try:
|
try:
|
||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
|
|||||||
@@ -1018,7 +1018,7 @@ function syncExchangePnl(force){
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("btn-sync-exchange-pnl")?.addEventListener("click", function(){
|
document.getElementById("btn-sync-exchange-pnl")?.addEventListener("click", function(){
|
||||||
if(confirm("从 Binance 流水回填盈亏(含手续费)?将覆盖未复盘记录的展示盈亏。")){
|
if(confirm("从 Binance 成交/流水回填盈亏(与 App 仓位历史口径一致,不含资金费)?将覆盖未复盘记录的展示盈亏。")){
|
||||||
syncExchangePnl(true);
|
syncExchangePnl(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user