修复持仓

This commit is contained in:
dekun
2026-05-31 12:32:06 +08:00
parent cdbe087202
commit ee8cd5caf0
4 changed files with 58 additions and 9 deletions
+14 -1
View File
@@ -3,8 +3,12 @@ from __future__ import annotations
import hashlib
import hmac
import time
from typing import Mapping
from urllib.parse import urlparse
# SOL 等合约 enable_decimal 时 size 可为 0.5;未带此头 Gate 会把小数张数向下取整为 0。
GATE_SIZE_DECIMAL_HEADER: dict[str, str] = {"X-Gate-Size-Decimal": "1"}
def gate_sign_path(api_base: str, path_rel: str) -> str:
"""签名用路径:/api/v4 + /futures/usdt/...(不含 host)。"""
@@ -28,10 +32,19 @@ def gate_sign_v4_headers(
hashed = m.hexdigest()
payload = f"{method.upper()}\n{sign_path}\n{query_string}\n{hashed}\n{ts}"
sign = hmac.new(api_secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha512).hexdigest()
return {
headers: dict[str, str] = {
"KEY": api_key,
"Timestamp": ts,
"SIGN": sign,
"Accept": "application/json",
"Content-Type": "application/json",
}
headers.update(GATE_SIZE_DECIMAL_HEADER)
return headers
def gate_public_headers(extra: Mapping[str, str] | None = None) -> dict[str, str]:
headers = {"Accept": "application/json", **GATE_SIZE_DECIMAL_HEADER}
if extra:
headers.update(extra)
return headers
+35 -7
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import asyncio
import json
import logging
import math
@@ -10,7 +11,7 @@ from typing import Any
import httpx
from .config import Settings
from .gate_auth import gate_sign_path, gate_sign_v4_headers
from .gate_auth import gate_public_headers, gate_sign_path, gate_sign_v4_headers
from .gate_price_rounding import _format_trigger_price, _trigger_price_tick
from .models_signal import TradeSignal
from .proxy_util import httpx_client_kwargs
@@ -47,7 +48,7 @@ class GateFuturesLive:
async def _public_get(self, rel: str, *, params: dict[str, str] | None = None) -> Any:
url = f"{self._base}{rel}"
async with httpx.AsyncClient(**self._kw) as client:
r = await client.get(url, params=params)
r = await client.get(url, params=params, headers=gate_public_headers())
r.raise_for_status()
return r.json()
@@ -95,16 +96,42 @@ class GateFuturesLive:
async def fetch_net_position_size(client: GateFuturesLive, contract: str) -> float:
"""该合约净持仓张数(单向模式 size 正负表示方向)。"""
ct = contract.strip().upper()
data = await client._signed("GET", f"{client._prefix}/positions")
data = await client._signed("GET", f"{client._prefix}/positions", query_string="holding=true")
if not isinstance(data, list):
return 0.0
for p in data:
if str(p.get("contract") or "").strip().upper() != ct:
continue
return _float(p.get("size"))
# 兜底:单合约查询(部分账户 holding 列表延迟时仍可取到)
try:
row = await client._signed("GET", f"{client._prefix}/positions/{ct}")
except RuntimeError:
return 0.0
if isinstance(row, dict):
return _float(row.get("size"))
return 0.0
async def fetch_net_position_size_with_retry(
client: GateFuturesLive,
contract: str,
*,
attempts: int = 4,
delay_sec: float = 0.25,
) -> float:
"""市价成交后持仓接口可能短暂滞后,带重试读取净张数。"""
n = max(1, int(attempts))
last = 0.0
for i in range(n):
last = await fetch_net_position_size(client, contract)
if abs(last) > 1e-12:
return last
if i + 1 < n:
await asyncio.sleep(delay_sec)
return last
async def post_stop_loss_price_order(
client: GateFuturesLive,
*,
@@ -190,7 +217,7 @@ def _float(x: Any, default: float = 0.0) -> float:
async def fetch_open_contracts(client: GateFuturesLive) -> set[str]:
rel = f"{client._prefix}/positions"
data = await client._signed("GET", rel)
data = await client._signed("GET", rel, query_string="holding=true")
if not isinstance(data, list):
return set()
out: set[str] = set()
@@ -288,9 +315,10 @@ def _market_fill_accepted(
st = str(order.get("status") or "")
finish = str(order.get("finish_as") or "")
if effective >= min_sz:
if st == "finished" or net_abs >= min_sz:
filled_ok = filled + 1e-12 >= min_sz
if st == "finished" or net_abs >= min_sz or filled_ok:
note = "partial_fill" if filled > 1e-12 and abs(_float(order.get("left"))) > 1e-12 else "filled"
if finish and finish not in {"filled", "ioc", ""} and net_abs < min_sz:
if finish and finish not in {"filled", "ioc", ""} and net_abs < min_sz and not filled_ok:
return False, effective, f"finish_as={finish}"
return True, effective, note
return False, effective, "no_fill"
@@ -369,7 +397,7 @@ async def execute_signal_live(settings: Settings, sig: TradeSignal) -> dict:
if not isinstance(order, dict):
return {"status": "error", "reason": "order_response_invalid"}
net_size = await fetch_net_position_size(client, contract)
net_size = await fetch_net_position_size_with_retry(client, contract)
fill_ok, filled_abs, fill_note = _market_fill_accepted(
order,
net_size=net_size,
+1 -1
View File
@@ -47,7 +47,7 @@ async def list_futures_positions(
cap = max(1, min(int(limit), 200))
try:
c = GateFuturesLive(settings)
data = await c._signed("GET", f"{c._prefix}/positions")
data = await c._signed("GET", f"{c._prefix}/positions", query_string="holding=true")
if not isinstance(data, list):
return None, f"unexpected_response:{type(data).__name__}"
out: list[dict[str, Any]] = []