Initial standalone crypto_okx with one-click deploy.
Add deploy/manage.sh bootstrap for git.bz121.com/dekun/crypto_okx and point docs at this repo. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Shared library package."""
|
||||
@@ -0,0 +1,187 @@
|
||||
"""Binance:交易账户 futures income;资金账户 deposits/withdrawals/transfers.USDT."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from lib.account_ledger.account_ledger_normalize import (
|
||||
ACCOUNT_FUNDING,
|
||||
ACCOUNT_TRADING,
|
||||
from_ccxt_ledger_entry,
|
||||
kind_from_raw,
|
||||
make_ref_id,
|
||||
normalize_row,
|
||||
)
|
||||
|
||||
|
||||
def _paginate_income(exchange, *, start_ms: int, end_ms: int, max_pages: int = 15) -> list[dict]:
|
||||
out: list[dict] = []
|
||||
cursor = int(start_ms)
|
||||
end = int(end_ms)
|
||||
for _ in range(max_pages):
|
||||
try:
|
||||
if hasattr(exchange, "fapiPrivateGetIncome"):
|
||||
batch = exchange.fapiPrivateGetIncome(
|
||||
{"startTime": cursor, "endTime": end, "limit": 1000}
|
||||
)
|
||||
else:
|
||||
batch = exchange.fetch_ledger(
|
||||
"USDT", cursor, 1000, {"type": "swap", "until": end}
|
||||
)
|
||||
# already unified
|
||||
return batch or []
|
||||
except Exception:
|
||||
break
|
||||
if not batch:
|
||||
break
|
||||
out.extend(batch)
|
||||
if len(batch) < 1000:
|
||||
break
|
||||
last_t = batch[-1].get("time") or batch[-1].get("timestamp")
|
||||
try:
|
||||
last_i = int(float(last_t))
|
||||
except Exception:
|
||||
break
|
||||
if last_i >= end:
|
||||
break
|
||||
cursor = last_i + 1
|
||||
return out
|
||||
|
||||
|
||||
def _income_to_row(raw: dict) -> Optional[dict[str, Any]]:
|
||||
if not isinstance(raw, dict):
|
||||
return None
|
||||
# raw fapi income
|
||||
if "income" in raw or "incomeType" in raw:
|
||||
amt = raw.get("income")
|
||||
ts = raw.get("time")
|
||||
ccy = raw.get("asset") or "USDT"
|
||||
raw_type = str(raw.get("incomeType") or "")
|
||||
ref = str(raw.get("tranId") or raw.get("tradeId") or "")
|
||||
return normalize_row(
|
||||
account=ACCOUNT_TRADING,
|
||||
ccy=str(ccy),
|
||||
amount=amt,
|
||||
ts_ms=ts,
|
||||
ref_id=ref or make_ref_id("trading", ccy, ts, amt, raw_type),
|
||||
raw_type=raw_type,
|
||||
symbol=str(raw.get("symbol") or ""),
|
||||
note=str(raw.get("info") or ""),
|
||||
kind=kind_from_raw(raw_type, float(amt) if amt is not None else None),
|
||||
)
|
||||
return from_ccxt_ledger_entry(raw, account=ACCOUNT_TRADING)
|
||||
|
||||
|
||||
def _dep_wd_to_row(entry: dict, *, kind: str) -> Optional[dict[str, Any]]:
|
||||
if not isinstance(entry, dict):
|
||||
return None
|
||||
info = entry.get("info") if isinstance(entry.get("info"), dict) else {}
|
||||
amount = entry.get("amount")
|
||||
ts = entry.get("timestamp") or info.get("insertTime") or info.get("applyTime")
|
||||
ccy = entry.get("currency") or info.get("coin") or "USDT"
|
||||
status = entry.get("status") or info.get("status") or ""
|
||||
ref = str(entry.get("id") or info.get("txId") or info.get("id") or "")
|
||||
amt = amount
|
||||
try:
|
||||
af = float(amount)
|
||||
if kind == "withdraw" and af > 0:
|
||||
af = -af
|
||||
amt = af
|
||||
except Exception:
|
||||
pass
|
||||
return normalize_row(
|
||||
account=ACCOUNT_FUNDING,
|
||||
ccy=str(ccy),
|
||||
amount=amt,
|
||||
ts_ms=ts,
|
||||
ref_id=ref or make_ref_id("funding", kind, ccy, ts, amount),
|
||||
raw_type=kind,
|
||||
note=str(status),
|
||||
kind=kind,
|
||||
)
|
||||
|
||||
|
||||
def _transfer_to_row(entry: dict) -> Optional[dict[str, Any]]:
|
||||
if not isinstance(entry, dict):
|
||||
return None
|
||||
info = entry.get("info") if isinstance(entry.get("info"), dict) else {}
|
||||
amount = entry.get("amount")
|
||||
ts = entry.get("timestamp") or info.get("timestamp")
|
||||
ccy = entry.get("currency") or info.get("asset") or "USDT"
|
||||
ref = str(entry.get("id") or info.get("tranId") or info.get("id") or "")
|
||||
frm = str(entry.get("fromAccount") or info.get("from") or "")
|
||||
to = str(entry.get("toAccount") or info.get("to") or "")
|
||||
try:
|
||||
amt = float(amount)
|
||||
except Exception:
|
||||
return None
|
||||
# 资金侧视角:从资金转出为负,转入为正(粗分)
|
||||
note = f"{frm}->{to}".strip("->")
|
||||
raw_type = "transfer"
|
||||
return normalize_row(
|
||||
account=ACCOUNT_FUNDING,
|
||||
ccy=str(ccy),
|
||||
amount=amt,
|
||||
ts_ms=ts,
|
||||
ref_id=ref or make_ref_id("funding", "transfer", ccy, ts, amt),
|
||||
raw_type=raw_type,
|
||||
note=note,
|
||||
kind=kind_from_raw("transfer", amt),
|
||||
)
|
||||
|
||||
|
||||
def fetch_binance_account_ledger(
|
||||
exchange,
|
||||
*,
|
||||
start_ms: int,
|
||||
end_ms: int,
|
||||
ensure_markets: Optional[Callable[[], None]] = None,
|
||||
) -> tuple[list[dict[str, Any]], list[str]]:
|
||||
errors: list[str] = []
|
||||
rows: list[dict[str, Any]] = []
|
||||
if ensure_markets:
|
||||
try:
|
||||
ensure_markets()
|
||||
except Exception as e:
|
||||
errors.append(f"markets:{e}")
|
||||
|
||||
# 交易账户
|
||||
try:
|
||||
raw = _paginate_income(exchange, start_ms=start_ms, end_ms=end_ms)
|
||||
for e in raw:
|
||||
n = _income_to_row(e)
|
||||
if n and n["ccy"] == "USDT":
|
||||
rows.append(n)
|
||||
except Exception as e:
|
||||
errors.append(f"trading:{e}")
|
||||
|
||||
# 资金账户:充提 + 划转
|
||||
for label, fn, kind in (
|
||||
("deposits", "fetch_deposits", "deposit"),
|
||||
("withdrawals", "fetch_withdrawals", "withdraw"),
|
||||
):
|
||||
try:
|
||||
meth = getattr(exchange, fn, None)
|
||||
if not callable(meth):
|
||||
continue
|
||||
batch = meth("USDT", int(start_ms), 1000, {"until": int(end_ms)}) or []
|
||||
for e in batch:
|
||||
n = _dep_wd_to_row(e, kind=kind)
|
||||
if n:
|
||||
rows.append(n)
|
||||
except Exception as e:
|
||||
errors.append(f"{label}:{e}")
|
||||
|
||||
try:
|
||||
if hasattr(exchange, "fetch_transfers"):
|
||||
batch = (
|
||||
exchange.fetch_transfers("USDT", int(start_ms), 1000, {"until": int(end_ms)})
|
||||
or []
|
||||
)
|
||||
for e in batch:
|
||||
n = _transfer_to_row(e)
|
||||
if n:
|
||||
rows.append(n)
|
||||
except Exception as e:
|
||||
errors.append(f"transfers:{e}")
|
||||
|
||||
return rows, errors
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Gate.io ccxt 构造(ccxt 4.x 起类名由 gateio 改为 gate)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ccxt
|
||||
|
||||
|
||||
def gate_ccxt_class():
|
||||
"""返回 ccxt Gate 交易所类(兼容旧版 gateio 名称)."""
|
||||
return getattr(ccxt, "gate", None) or ccxt.gateio
|
||||
@@ -0,0 +1,212 @@
|
||||
"""Gate:资金账户(spot account_book) + 交易账户(futures account_book),USDT."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from lib.account_ledger.account_ledger_normalize import (
|
||||
ACCOUNT_FUNDING,
|
||||
ACCOUNT_TRADING,
|
||||
from_ccxt_ledger_entry,
|
||||
kind_from_raw,
|
||||
make_ref_id,
|
||||
normalize_row,
|
||||
)
|
||||
|
||||
|
||||
def _sec(ms: int) -> int:
|
||||
return max(0, int(int(ms) // 1000))
|
||||
|
||||
|
||||
def _paginate_spot_book(exchange, *, start_ms: int, end_ms: int, max_pages: int = 10) -> list[dict]:
|
||||
out: list[dict] = []
|
||||
# Gate spot account_book: from/to 为秒
|
||||
cursor = _sec(start_ms)
|
||||
end = _sec(end_ms)
|
||||
for _ in range(max_pages):
|
||||
try:
|
||||
batch = exchange.privateSpotGetAccountBook(
|
||||
{
|
||||
"currency": "USDT",
|
||||
"from": cursor,
|
||||
"to": end,
|
||||
"limit": 100,
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
break
|
||||
if not batch:
|
||||
break
|
||||
if isinstance(batch, dict):
|
||||
batch = batch.get("data") or batch.get("result") or []
|
||||
if not isinstance(batch, list) or not batch:
|
||||
break
|
||||
out.extend(batch)
|
||||
if len(batch) < 100:
|
||||
break
|
||||
last_t = batch[-1].get("time") or batch[-1].get("create_time")
|
||||
try:
|
||||
last_i = int(float(last_t))
|
||||
except Exception:
|
||||
break
|
||||
# spot 返回秒
|
||||
if last_i > 1e12:
|
||||
last_i = last_i // 1000
|
||||
if last_i >= end:
|
||||
break
|
||||
cursor = last_i + 1
|
||||
return out
|
||||
|
||||
|
||||
def _paginate_swap_book(exchange, *, start_ms: int, end_ms: int, max_pages: int = 10) -> list[dict]:
|
||||
out: list[dict] = []
|
||||
cursor = _sec(start_ms)
|
||||
end = _sec(end_ms)
|
||||
for _ in range(max_pages):
|
||||
try:
|
||||
batch = exchange.privateFuturesGetSettleAccountBook(
|
||||
{
|
||||
"settle": "usdt",
|
||||
"from": cursor,
|
||||
"to": end,
|
||||
"limit": 100,
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
break
|
||||
if not batch:
|
||||
break
|
||||
if isinstance(batch, dict):
|
||||
batch = batch.get("data") or batch.get("result") or []
|
||||
if not isinstance(batch, list) or not batch:
|
||||
break
|
||||
out.extend(batch)
|
||||
if len(batch) < 100:
|
||||
break
|
||||
last_t = batch[-1].get("time")
|
||||
try:
|
||||
last_i = int(float(last_t))
|
||||
except Exception:
|
||||
break
|
||||
if last_i > 1e12:
|
||||
last_i = last_i // 1000
|
||||
if last_i >= end:
|
||||
break
|
||||
cursor = last_i + 1
|
||||
return out
|
||||
|
||||
|
||||
def _spot_row(raw: dict) -> Optional[dict[str, Any]]:
|
||||
if not isinstance(raw, dict):
|
||||
return None
|
||||
amt = raw.get("change")
|
||||
ts = raw.get("time") or raw.get("create_time")
|
||||
# 秒 → 毫秒
|
||||
try:
|
||||
t = float(ts)
|
||||
if t < 1e12:
|
||||
t = t * 1000.0
|
||||
ts = t
|
||||
except Exception:
|
||||
pass
|
||||
raw_type = str(raw.get("type") or raw.get("change_type") or "")
|
||||
bal = raw.get("balance")
|
||||
ref = str(raw.get("id") or raw.get("txid") or "")
|
||||
return normalize_row(
|
||||
account=ACCOUNT_FUNDING,
|
||||
ccy="USDT",
|
||||
amount=amt,
|
||||
ts_ms=ts,
|
||||
ref_id=ref or make_ref_id("funding", raw_type, ts, amt),
|
||||
raw_type=raw_type,
|
||||
balance_after=bal,
|
||||
note=str(raw.get("text") or ""),
|
||||
kind=kind_from_raw(raw_type, float(amt) if amt is not None else None),
|
||||
)
|
||||
|
||||
|
||||
def _swap_row(raw: dict) -> Optional[dict[str, Any]]:
|
||||
if not isinstance(raw, dict):
|
||||
return None
|
||||
# futures account_book: change, balance, type, text, time, contract...
|
||||
amt = raw.get("change")
|
||||
ts = raw.get("time")
|
||||
try:
|
||||
t = float(ts)
|
||||
if t < 1e12:
|
||||
t = t * 1000.0
|
||||
ts = t
|
||||
except Exception:
|
||||
pass
|
||||
raw_type = str(raw.get("type") or "")
|
||||
bal = raw.get("balance")
|
||||
ref = str(raw.get("id") or "")
|
||||
return normalize_row(
|
||||
account=ACCOUNT_TRADING,
|
||||
ccy="USDT",
|
||||
amount=amt,
|
||||
ts_ms=ts,
|
||||
ref_id=ref or make_ref_id("trading", raw_type, ts, amt, raw.get("contract")),
|
||||
raw_type=raw_type,
|
||||
balance_after=bal,
|
||||
symbol=str(raw.get("contract") or ""),
|
||||
note=str(raw.get("text") or ""),
|
||||
kind=kind_from_raw(raw_type, float(amt) if amt is not None else None),
|
||||
)
|
||||
|
||||
|
||||
def fetch_gate_account_ledger(
|
||||
exchange,
|
||||
*,
|
||||
start_ms: int,
|
||||
end_ms: int,
|
||||
ensure_markets: Optional[Callable[[], None]] = None,
|
||||
) -> tuple[list[dict[str, Any]], list[str]]:
|
||||
errors: list[str] = []
|
||||
rows: list[dict[str, Any]] = []
|
||||
if ensure_markets:
|
||||
try:
|
||||
ensure_markets()
|
||||
except Exception as e:
|
||||
errors.append(f"markets:{e}")
|
||||
|
||||
try:
|
||||
for e in _paginate_spot_book(exchange, start_ms=start_ms, end_ms=end_ms):
|
||||
n = _spot_row(e)
|
||||
if n:
|
||||
rows.append(n)
|
||||
except Exception as e:
|
||||
errors.append(f"funding:{e}")
|
||||
# 回退 ccxt fetch_ledger
|
||||
try:
|
||||
batch = exchange.fetch_ledger(
|
||||
"USDT", int(start_ms), 100, {"type": "spot", "until": int(end_ms)}
|
||||
) or []
|
||||
for e in batch:
|
||||
n = from_ccxt_ledger_entry(e, account=ACCOUNT_FUNDING)
|
||||
if n:
|
||||
rows.append(n)
|
||||
except Exception as e2:
|
||||
errors.append(f"funding_fallback:{e2}")
|
||||
|
||||
try:
|
||||
for e in _paginate_swap_book(exchange, start_ms=start_ms, end_ms=end_ms):
|
||||
n = _swap_row(e)
|
||||
if n:
|
||||
rows.append(n)
|
||||
except Exception as e:
|
||||
errors.append(f"trading:{e}")
|
||||
try:
|
||||
batch = exchange.fetch_ledger(
|
||||
"USDT",
|
||||
int(start_ms),
|
||||
100,
|
||||
{"type": "swap", "settle": "usdt", "until": int(end_ms)},
|
||||
) or []
|
||||
for e in batch:
|
||||
n = from_ccxt_ledger_entry(e, account=ACCOUNT_TRADING)
|
||||
if n:
|
||||
rows.append(n)
|
||||
except Exception as e2:
|
||||
errors.append(f"trading_fallback:{e2}")
|
||||
|
||||
return rows, errors
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Gate 平仓历史匹配(fetch_positions_history),供 reconcile / 中控全平同步共用."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def unified_symbol_for_match(symbol_str: str) -> str:
|
||||
x = (symbol_str or "").strip().upper()
|
||||
if ":" in x:
|
||||
x = x.split(":")[0]
|
||||
return x
|
||||
|
||||
|
||||
def pick_gate_position_close(
|
||||
hist: list[dict],
|
||||
symbol: str,
|
||||
direction: str,
|
||||
*,
|
||||
opened_at_ms: int | None = None,
|
||||
closed_at_ms: int | None = None,
|
||||
used_keys: set[str] | None = None,
|
||||
max_close_delta_ms: int = 25 * 60 * 1000,
|
||||
) -> dict | None:
|
||||
"""
|
||||
从 Gate 平仓历史列表中选取与 symbol/direction/开仓时间最匹配的一条.
|
||||
返回 normalize 后的 dict(含 close_ms,pnl,sync_key 等),无匹配则 None.
|
||||
"""
|
||||
if not hist:
|
||||
return None
|
||||
sym_u = unified_symbol_for_match(symbol)
|
||||
dir_l = (direction or "long").strip().lower()
|
||||
if dir_l not in ("long", "short"):
|
||||
return None
|
||||
used = used_keys or set()
|
||||
ref_ms = closed_at_ms or opened_at_ms
|
||||
best = None
|
||||
best_d = None
|
||||
for h in hist:
|
||||
if not isinstance(h, dict):
|
||||
continue
|
||||
sk = h.get("sync_key")
|
||||
if not sk or sk in used:
|
||||
continue
|
||||
if h.get("symbol_u") != sym_u:
|
||||
continue
|
||||
if (h.get("side") or "").strip().lower() != dir_l:
|
||||
continue
|
||||
cm = h.get("close_ms")
|
||||
if cm is None:
|
||||
continue
|
||||
if opened_at_ms is not None:
|
||||
if cm < opened_at_ms - 15 * 60 * 1000:
|
||||
continue
|
||||
if cm > opened_at_ms + 15 * 86400 * 1000:
|
||||
continue
|
||||
if ref_ms is not None:
|
||||
d = abs(int(cm) - int(ref_ms))
|
||||
else:
|
||||
d = 0
|
||||
if best_d is None or d < best_d:
|
||||
best_d = d
|
||||
best = h
|
||||
if best is None or best_d is None:
|
||||
return None
|
||||
if ref_ms is not None and best_d > max_close_delta_ms:
|
||||
return None
|
||||
return best
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Gate.io 资金划转(crypto_monitor_gate 共用)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
INVALID_KEY_HINT = (
|
||||
".常见原因:① GATE_API_SECRET 错误或 .env 里多了空格/换行;② IP 白名单未包含当前服务器出口 IP;"
|
||||
"③ Gate「交易账户」类 API Key 若不支持钱包接口则无法走账户内划转 POST /wallet/transfers(需在官网确认该 Key 类型是否开放划转);"
|
||||
"④ Key 已重置或权限变更.你已勾选现货/统一账户仍报错时,优先核对 Secret 与白名单."
|
||||
)
|
||||
|
||||
|
||||
def execute_transfer_usdt(
|
||||
exchange,
|
||||
amount: float,
|
||||
from_account: str,
|
||||
to_account: str,
|
||||
*,
|
||||
transfer_ccy: str = "USDT",
|
||||
ensure_live_ready: Callable[[], tuple[bool, str]],
|
||||
ensure_markets_loaded: Optional[Callable[[], None]] = None,
|
||||
) -> tuple[bool, str, Any]:
|
||||
if amount <= 0:
|
||||
return False, "划转金额必须大于0", None
|
||||
ccy = (transfer_ccy or "USDT").strip().upper() or "USDT"
|
||||
ok_live, reason = ensure_live_ready()
|
||||
if not ok_live:
|
||||
return False, reason, None
|
||||
if ensure_markets_loaded:
|
||||
try:
|
||||
ensure_markets_loaded()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
resp = exchange.transfer(ccy, float(amount), from_account, to_account)
|
||||
return True, "划转成功", resp
|
||||
except Exception as e:
|
||||
msg = str(e)
|
||||
if "INVALID_KEY" in msg or "Invalid key" in msg:
|
||||
msg += INVALID_KEY_HINT
|
||||
return False, msg, None
|
||||
|
||||
|
||||
def count_auto_transfer_blockers(conn, *, count_order_monitors: Callable[[Any], int]) -> int:
|
||||
"""自动划转持仓守卫:order_monitors active + 趋势回调已开仓计划."""
|
||||
n = int(count_order_monitors(conn) or 0)
|
||||
if n > 0:
|
||||
return n
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT COUNT(*) FROM trend_pullback_plans "
|
||||
"WHERE status='active' AND COALESCE(first_order_done, 0) != 0"
|
||||
).fetchone()
|
||||
return int(row[0] or 0) if row else 0
|
||||
except Exception:
|
||||
return n
|
||||
@@ -0,0 +1,99 @@
|
||||
"""OKX:资金账户 asset bills + 交易账户 account bills;USDT + USDC."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from lib.account_ledger.account_ledger_normalize import (
|
||||
ACCOUNT_FUNDING,
|
||||
ACCOUNT_TRADING,
|
||||
from_ccxt_ledger_entry,
|
||||
)
|
||||
|
||||
OKX_LEDGER_CCYS = ("USDT", "USDC")
|
||||
|
||||
|
||||
def _fetch_one(
|
||||
exchange,
|
||||
*,
|
||||
code: str,
|
||||
since: int,
|
||||
until: int,
|
||||
method: str,
|
||||
max_pages: int = 10,
|
||||
) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
after = None
|
||||
for _ in range(max_pages):
|
||||
params: dict[str, Any] = {"method": method, "until": int(until)}
|
||||
if after:
|
||||
params["after"] = after
|
||||
try:
|
||||
batch = exchange.fetch_ledger(code, int(since), 100, params) or []
|
||||
except Exception:
|
||||
# archive / bills 窗口差异:失败则停
|
||||
break
|
||||
if not batch:
|
||||
break
|
||||
out.extend(batch)
|
||||
if len(batch) < 100:
|
||||
break
|
||||
# OKX 翻页用 billId
|
||||
last = batch[-1]
|
||||
info = last.get("info") if isinstance(last.get("info"), dict) else {}
|
||||
bid = last.get("id") or info.get("billId")
|
||||
if not bid:
|
||||
break
|
||||
after = str(bid)
|
||||
return out
|
||||
|
||||
|
||||
def fetch_okx_account_ledger(
|
||||
exchange,
|
||||
*,
|
||||
start_ms: int,
|
||||
end_ms: int,
|
||||
ensure_markets: Optional[Callable[[], None]] = None,
|
||||
) -> tuple[list[dict[str, Any]], list[str]]:
|
||||
errors: list[str] = []
|
||||
rows: list[dict[str, Any]] = []
|
||||
if ensure_markets:
|
||||
try:
|
||||
ensure_markets()
|
||||
except Exception as e:
|
||||
errors.append(f"markets:{e}")
|
||||
|
||||
for ccy in OKX_LEDGER_CCYS:
|
||||
# 资金账户
|
||||
try:
|
||||
raw = _fetch_one(
|
||||
exchange,
|
||||
code=ccy,
|
||||
since=start_ms,
|
||||
until=end_ms,
|
||||
method="privateGetAssetBills",
|
||||
)
|
||||
for e in raw:
|
||||
n = from_ccxt_ledger_entry(e, account=ACCOUNT_FUNDING)
|
||||
if n:
|
||||
rows.append(n)
|
||||
except Exception as e:
|
||||
errors.append(f"funding:{ccy}:{e}")
|
||||
|
||||
# 交易账户:近 3 月 archive + 近 7 日 bills(去重靠 upsert)
|
||||
for method in ("privateGetAccountBillsArchive", "privateGetAccountBills"):
|
||||
try:
|
||||
raw = _fetch_one(
|
||||
exchange,
|
||||
code=ccy,
|
||||
since=start_ms,
|
||||
until=end_ms,
|
||||
method=method,
|
||||
)
|
||||
for e in raw:
|
||||
n = from_ccxt_ledger_entry(e, account=ACCOUNT_TRADING)
|
||||
if n:
|
||||
rows.append(n)
|
||||
except Exception as e:
|
||||
errors.append(f"trading:{ccy}:{method}:{e}")
|
||||
|
||||
return rows, errors
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
OKX 挂单聚合:普通委托 + 算法单(conditional / oco / trigger).
|
||||
交易所 App「止盈止损」页多为 orders-algo-pending,仅 fetch_open_orders 默认拿不到.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _order_dedupe_key(order: dict) -> str:
|
||||
info = order.get("info") or {}
|
||||
if not isinstance(info, dict):
|
||||
info = {}
|
||||
return str(order.get("id") or info.get("algoId") or info.get("ordId") or "")
|
||||
|
||||
|
||||
def _okx_algo_cancel_id(order_id: str) -> str:
|
||||
oid = str(order_id or "")
|
||||
if ":" in oid:
|
||||
return oid.split(":", 1)[0]
|
||||
return oid
|
||||
|
||||
|
||||
def _okx_order_needs_stop_cancel_param(order: dict) -> bool:
|
||||
"""OKX 条件/算法单撤单须 params.stop=True,否则 cancel_order 走普通单接口会静默失败."""
|
||||
if not isinstance(order, dict):
|
||||
return False
|
||||
info = order.get("info") or {}
|
||||
if not isinstance(info, dict):
|
||||
info = {}
|
||||
if order.get("stopLossPrice") is not None or order.get("takeProfitPrice") is not None:
|
||||
return True
|
||||
if info.get("algoId") or info.get("slTriggerPx") or info.get("tpTriggerPx"):
|
||||
return True
|
||||
typ = str(order.get("type") or info.get("ordType") or "").lower()
|
||||
for token in ("conditional", "oco", "trigger", "move_order_stop", "iceberg"):
|
||||
if token in typ:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def fetch_okx_all_open_orders(ex, exchange_symbol: str) -> list[dict]:
|
||||
"""合并 OKX 普通挂单与算法挂单(去重)."""
|
||||
if not exchange_symbol:
|
||||
return []
|
||||
ex.load_markets()
|
||||
sym = exchange_symbol
|
||||
try:
|
||||
sym = ex.market(exchange_symbol)["symbol"]
|
||||
except Exception:
|
||||
pass
|
||||
seen: set[str] = set()
|
||||
out: list[dict] = []
|
||||
|
||||
def add_batch(batch: list | None) -> None:
|
||||
for o in batch or []:
|
||||
if not isinstance(o, dict):
|
||||
continue
|
||||
k = _order_dedupe_key(o)
|
||||
if not k or k in seen:
|
||||
continue
|
||||
seen.add(k)
|
||||
out.append(o)
|
||||
|
||||
try:
|
||||
add_batch(ex.fetch_open_orders(sym))
|
||||
except Exception:
|
||||
pass
|
||||
for params in (
|
||||
{"ordType": "conditional"},
|
||||
{"ordType": "oco"},
|
||||
{"trigger": True},
|
||||
):
|
||||
try:
|
||||
add_batch(ex.fetch_open_orders(sym, params=dict(params)))
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def cancel_okx_all_open_orders(ex, exchange_symbol: str) -> int:
|
||||
"""
|
||||
撤销某合约全部挂单(普通 + 条件/算法).
|
||||
OKX 止盈止损在 orders-algo-pending,必须用 stop=True 才能撤掉.
|
||||
"""
|
||||
if not exchange_symbol:
|
||||
return 0
|
||||
ex.load_markets()
|
||||
sym = exchange_symbol
|
||||
try:
|
||||
sym = ex.market(exchange_symbol)["symbol"]
|
||||
except Exception:
|
||||
pass
|
||||
n = 0
|
||||
for o in fetch_okx_all_open_orders(ex, sym):
|
||||
oid = _order_dedupe_key(o)
|
||||
if not oid:
|
||||
continue
|
||||
cancel_id = _okx_algo_cancel_id(oid)
|
||||
params = {"stop": True} if _okx_order_needs_stop_cancel_param(o) else None
|
||||
try:
|
||||
ex.cancel_order(cancel_id, sym, params)
|
||||
n += 1
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
ex.cancel_order(oid, sym, params)
|
||||
n += 1
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
ex.cancel_all_orders(sym)
|
||||
except Exception:
|
||||
pass
|
||||
return n
|
||||
Reference in New Issue
Block a user