修复持仓
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]] = []
|
||||
|
||||
Reference in New Issue
Block a user