修复okx止盈止损

This commit is contained in:
dekun
2026-05-25 11:25:14 +08:00
parent 436d2aef6c
commit 1e36086465
7 changed files with 211 additions and 16 deletions
+118 -11
View File
@@ -37,13 +37,22 @@ from hub_web_auth import (
)
from url_public import browser_url, default_review_url, public_origin
try:
from exchange_orders import symbols_match as _symbols_match
except ImportError:
def _symbols_match(position_symbol: str, order_symbol: str) -> bool:
a = (position_symbol or "").strip().upper()
b = (order_symbol or "").strip().upper()
return bool(a and b and a == b)
HUB_HOST = os.getenv("HUB_HOST", "0.0.0.0")
HUB_PORT = int(os.getenv("HUB_PORT", "5100"))
HUB_BRIDGE_TOKEN = (os.getenv("HUB_BRIDGE_TOKEN") or os.getenv("CONTROL_TOKEN") or "").strip()
_trust_raw = (os.getenv("HUB_TRUST_LAN", "true") or "").strip().lower()
HUB_TRUST_LAN = _trust_raw not in ("0", "false", "no", "off")
DIR = Path(__file__).resolve().parent
HUB_BUILD = "20260525-okx-tpsl"
HUB_BUILD = "20260525-okx-tpsl2"
HUB_AGENT_TIMEOUT = float(os.getenv("HUB_AGENT_TIMEOUT", "8"))
HUB_FLASK_TIMEOUT = float(os.getenv("HUB_FLASK_TIMEOUT", "10"))
_board_key_prices_raw = (os.getenv("HUB_BOARD_KEY_PRICES", "true") or "").strip().lower()
@@ -352,33 +361,131 @@ def _flask_error_from_hub_mon(hub_mon: dict | None) -> str | None:
)
def _tpsl_slots_to_conditional_orders(exchange_tpsl: dict, symbol: str) -> list[dict]:
"""将实例 price_snapshot 的 exchange_tpsl 转为中控条件单结构。"""
out: list[dict] = []
if not isinstance(exchange_tpsl, dict):
return out
for role, label in (("sl", "止损"), ("tp", "止盈")):
slot = exchange_tpsl.get(role)
if not isinstance(slot, dict):
continue
trig = slot.get("trigger_price")
oid = slot.get("order_id")
if trig is None or oid is None:
continue
try:
trig_f = float(trig)
except (TypeError, ValueError):
continue
out.append(
{
"id": str(oid),
"symbol": symbol,
"channel": "algo",
"category": "conditional",
"label": f"{label} {trig_f:g}",
"trigger_price": trig_f,
"amount": None,
"status": "open",
}
)
return out
def _find_exchange_tpsl_for_position(
symbol: str,
side: str,
order_prices: list,
hub_orders: list,
) -> dict | None:
side_l = (side or "").lower()
op_by_id = {
op.get("id"): op
for op in order_prices
if isinstance(op, dict) and op.get("id") is not None
}
for o in hub_orders:
if not isinstance(o, dict):
continue
o_sym = o.get("exchange_symbol") or o.get("symbol") or ""
if not _symbols_match(symbol, o_sym):
continue
if (o.get("direction") or "").lower() != side_l:
continue
op = op_by_id.get(o.get("id"))
if not isinstance(op, dict):
continue
et = op.get("exchange_tpsl")
if isinstance(et, dict) and (et.get("sl") or et.get("tp")):
return et
for op in order_prices:
if not isinstance(op, dict):
continue
if not _symbols_match(symbol, op.get("symbol") or ""):
continue
et = op.get("exchange_tpsl")
if isinstance(et, dict) and (et.get("sl") or et.get("tp")):
return et
return None
def _merge_flask_exchange_tpsl(agent_row: dict, snap: dict | None, hub_mon: dict | None) -> None:
"""子代理挂单为空时,用实例 Flask 已算好的 exchange_tpsl 补全展示。"""
ag = agent_row.get("agent")
if not isinstance(ag, dict):
return
positions = ag.get("positions")
if not isinstance(positions, list) or not positions:
return
if not isinstance(snap, dict):
return
order_prices = snap.get("order_prices") or []
hub_orders = []
if isinstance(hub_mon, dict):
hub_orders = hub_mon.get("orders") or []
for p in positions:
if not isinstance(p, dict):
continue
sym = p.get("symbol") or ""
side = p.get("side") or ""
et = _find_exchange_tpsl_for_position(sym, side, order_prices, hub_orders)
if not et:
continue
p["exchange_tpsl"] = et
cond = p.get("conditional_orders") or []
if not cond:
p["conditional_orders"] = _tpsl_slots_to_conditional_orders(et, sym)
async def _fetch_exchange_flask_bundle(
client: httpx.AsyncClient, ex: dict
) -> tuple[dict | None, dict | None, list | None]:
"""单所 Flaskmonitor / meta /(可选)price_snapshot 并行拉取。"""
) -> tuple[dict | None, dict | None, list | None, dict | None]:
"""单所 Flaskmonitor / meta / price_snapshot(有 flask_url 时)并行拉取。"""
caps = ex.get("capabilities") or []
tasks = [
_fetch_flask_json(client, ex, "/api/hub/monitor"),
_fetch_flask_json(client, ex, "/api/hub/meta"),
]
want_prices = HUB_BOARD_KEY_PRICES and "key" in caps
if want_prices:
has_flask = bool((ex.get("flask_url") or "").strip())
if has_flask:
tasks.append(_fetch_flask_json(client, ex, "/api/price_snapshot"))
results = await asyncio.gather(*tasks)
hub_mon = results[0]
meta = results[1]
snap = results[2] if has_flask and len(results) > 2 else None
key_prices = None
if want_prices and len(results) > 2:
snap = results[2]
if isinstance(snap, dict):
key_prices = snap.get("key_prices")
return hub_mon, meta, key_prices
want_prices = HUB_BOARD_KEY_PRICES and "key" in caps
if want_prices and isinstance(snap, dict):
key_prices = snap.get("key_prices")
return hub_mon, meta, key_prices, snap if isinstance(snap, dict) else None
async def _assemble_board_row(
client: httpx.AsyncClient, ex: dict, agent_row: dict
) -> dict:
hub_mon, meta, key_prices = await _fetch_exchange_flask_bundle(client, ex)
hub_mon, meta, key_prices, snap = await _fetch_exchange_flask_bundle(client, ex)
_merge_flask_exchange_tpsl(agent_row, snap, hub_mon if isinstance(hub_mon, dict) else None)
flask_ok = isinstance(hub_mon, dict) and hub_mon.get("ok") is not False
raw_review = (ex.get("review_url") or "").strip()
review_link = browser_url(raw_review) if raw_review else default_review_url(