修复okx止盈止损
This commit is contained in:
@@ -550,12 +550,14 @@ def _status_inner(x_control_token: str | None) -> Any:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
orders_fetch_error: str | None = None
|
||||||
try:
|
try:
|
||||||
attach_orders_to_positions(
|
attach_orders_to_positions(
|
||||||
positions_out,
|
positions_out,
|
||||||
list_open_orders(ex, EXCHANGE_KIND, None),
|
list_open_orders(ex, EXCHANGE_KIND, None),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
orders_fetch_error = str(e)
|
||||||
for p in positions_out:
|
for p in positions_out:
|
||||||
p.setdefault("conditional_orders", [])
|
p.setdefault("conditional_orders", [])
|
||||||
p.setdefault("regular_orders", [])
|
p.setdefault("regular_orders", [])
|
||||||
@@ -564,7 +566,7 @@ def _status_inner(x_control_token: str | None) -> Any:
|
|||||||
pm = _position_mode_label()
|
pm = _position_mode_label()
|
||||||
except Exception:
|
except Exception:
|
||||||
pm = EXCHANGE_KIND
|
pm = EXCHANGE_KIND
|
||||||
return {
|
out = {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"exchange": EXCHANGE_KIND,
|
"exchange": EXCHANGE_KIND,
|
||||||
"balance_usdt": balance_usdt,
|
"balance_usdt": balance_usdt,
|
||||||
@@ -572,6 +574,9 @@ def _status_inner(x_control_token: str | None) -> Any:
|
|||||||
"total_unrealized_pnl": _finite_or_none(total_upnl),
|
"total_unrealized_pnl": _finite_or_none(total_upnl),
|
||||||
"position_mode": pm,
|
"position_mode": pm,
|
||||||
}
|
}
|
||||||
|
if orders_fetch_error:
|
||||||
|
out["orders_fetch_error"] = orders_fetch_error
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
@app.get("/open-orders")
|
@app.get("/open-orders")
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ function agentApp(name, exchangeDir, exchange, port) {
|
|||||||
EXCHANGE: exchange,
|
EXCHANGE: exchange,
|
||||||
PORT: String(port),
|
PORT: String(port),
|
||||||
HOST: "127.0.0.1",
|
HOST: "127.0.0.1",
|
||||||
|
PYTHONPATH: REPO_ROOT,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+118
-11
@@ -37,13 +37,22 @@ from hub_web_auth import (
|
|||||||
)
|
)
|
||||||
from url_public import browser_url, default_review_url, public_origin
|
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_HOST = os.getenv("HUB_HOST", "0.0.0.0")
|
||||||
HUB_PORT = int(os.getenv("HUB_PORT", "5100"))
|
HUB_PORT = int(os.getenv("HUB_PORT", "5100"))
|
||||||
HUB_BRIDGE_TOKEN = (os.getenv("HUB_BRIDGE_TOKEN") or os.getenv("CONTROL_TOKEN") or "").strip()
|
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()
|
_trust_raw = (os.getenv("HUB_TRUST_LAN", "true") or "").strip().lower()
|
||||||
HUB_TRUST_LAN = _trust_raw not in ("0", "false", "no", "off")
|
HUB_TRUST_LAN = _trust_raw not in ("0", "false", "no", "off")
|
||||||
DIR = Path(__file__).resolve().parent
|
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_AGENT_TIMEOUT = float(os.getenv("HUB_AGENT_TIMEOUT", "8"))
|
||||||
HUB_FLASK_TIMEOUT = float(os.getenv("HUB_FLASK_TIMEOUT", "10"))
|
HUB_FLASK_TIMEOUT = float(os.getenv("HUB_FLASK_TIMEOUT", "10"))
|
||||||
_board_key_prices_raw = (os.getenv("HUB_BOARD_KEY_PRICES", "true") or "").strip().lower()
|
_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(
|
async def _fetch_exchange_flask_bundle(
|
||||||
client: httpx.AsyncClient, ex: dict
|
client: httpx.AsyncClient, ex: dict
|
||||||
) -> tuple[dict | None, dict | None, list | None]:
|
) -> tuple[dict | None, dict | None, list | None, dict | None]:
|
||||||
"""单所 Flask:monitor / meta /(可选)price_snapshot 并行拉取。"""
|
"""单所 Flask:monitor / meta / price_snapshot(有 flask_url 时)并行拉取。"""
|
||||||
caps = ex.get("capabilities") or []
|
caps = ex.get("capabilities") or []
|
||||||
tasks = [
|
tasks = [
|
||||||
_fetch_flask_json(client, ex, "/api/hub/monitor"),
|
_fetch_flask_json(client, ex, "/api/hub/monitor"),
|
||||||
_fetch_flask_json(client, ex, "/api/hub/meta"),
|
_fetch_flask_json(client, ex, "/api/hub/meta"),
|
||||||
]
|
]
|
||||||
want_prices = HUB_BOARD_KEY_PRICES and "key" in caps
|
has_flask = bool((ex.get("flask_url") or "").strip())
|
||||||
if want_prices:
|
if has_flask:
|
||||||
tasks.append(_fetch_flask_json(client, ex, "/api/price_snapshot"))
|
tasks.append(_fetch_flask_json(client, ex, "/api/price_snapshot"))
|
||||||
results = await asyncio.gather(*tasks)
|
results = await asyncio.gather(*tasks)
|
||||||
hub_mon = results[0]
|
hub_mon = results[0]
|
||||||
meta = results[1]
|
meta = results[1]
|
||||||
|
snap = results[2] if has_flask and len(results) > 2 else None
|
||||||
key_prices = None
|
key_prices = None
|
||||||
if want_prices and len(results) > 2:
|
want_prices = HUB_BOARD_KEY_PRICES and "key" in caps
|
||||||
snap = results[2]
|
if want_prices and isinstance(snap, dict):
|
||||||
if isinstance(snap, dict):
|
key_prices = snap.get("key_prices")
|
||||||
key_prices = snap.get("key_prices")
|
return hub_mon, meta, key_prices, snap if isinstance(snap, dict) else None
|
||||||
return hub_mon, meta, key_prices
|
|
||||||
|
|
||||||
|
|
||||||
async def _assemble_board_row(
|
async def _assemble_board_row(
|
||||||
client: httpx.AsyncClient, ex: dict, agent_row: dict
|
client: httpx.AsyncClient, ex: dict, agent_row: dict
|
||||||
) -> 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
|
flask_ok = isinstance(hub_mon, dict) and hub_mon.get("ok") is not False
|
||||||
raw_review = (ex.get("review_url") or "").strip()
|
raw_review = (ex.get("review_url") or "").strip()
|
||||||
review_link = browser_url(raw_review) if raw_review else default_review_url(
|
review_link = browser_url(raw_review) if raw_review else default_review_url(
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""
|
||||||
|
OKX 挂单聚合(子代理本地副本,避免依赖仓库根 PYTHONPATH)。
|
||||||
|
普通委托 + 算法单 conditional / oco / trigger。
|
||||||
|
"""
|
||||||
|
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 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
|
||||||
@@ -4,6 +4,8 @@ set -e
|
|||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|
||||||
HUB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
HUB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
REPO_ROOT="$(cd "${HUB_DIR}/.." && pwd)"
|
||||||
|
export PYTHONPATH="${REPO_ROOT}${PYTHONPATH:+:${PYTHONPATH}}"
|
||||||
# shellcheck source=lib_load_dotenv.sh
|
# shellcheck source=lib_load_dotenv.sh
|
||||||
source "${HUB_DIR}/scripts/lib_load_dotenv.sh"
|
source "${HUB_DIR}/scripts/lib_load_dotenv.sh"
|
||||||
|
|
||||||
|
|||||||
@@ -168,6 +168,33 @@
|
|||||||
return localStorage.getItem(ordersCollapseKey(exchangeId, symbol)) === "1";
|
return localStorage.getItem(ordersCollapseKey(exchangeId, symbol)) === "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function condOrdersFromPosition(pos) {
|
||||||
|
const cond = Array.isArray(pos.conditional_orders) ? pos.conditional_orders : [];
|
||||||
|
if (cond.length) return cond;
|
||||||
|
const et = pos.exchange_tpsl;
|
||||||
|
if (!et) return [];
|
||||||
|
const out = [];
|
||||||
|
if (et.sl && et.sl.trigger_price != null) {
|
||||||
|
out.push({
|
||||||
|
label: "止损",
|
||||||
|
trigger_price: Number(et.sl.trigger_price),
|
||||||
|
amount: null,
|
||||||
|
id: et.sl.order_id,
|
||||||
|
channel: "algo",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (et.tp && et.tp.trigger_price != null) {
|
||||||
|
out.push({
|
||||||
|
label: "止盈",
|
||||||
|
trigger_price: Number(et.tp.trigger_price),
|
||||||
|
amount: null,
|
||||||
|
id: et.tp.order_id,
|
||||||
|
channel: "algo",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
function findMonitorOrder(orders, symbol, side) {
|
function findMonitorOrder(orders, symbol, side) {
|
||||||
const want = (side || "").toLowerCase();
|
const want = (side || "").toLowerCase();
|
||||||
for (const o of orders || []) {
|
for (const o of orders || []) {
|
||||||
@@ -452,7 +479,7 @@
|
|||||||
const sideCn = side === "long" ? "做多" : "做空";
|
const sideCn = side === "long" ? "做多" : "做空";
|
||||||
const sideCls = side === "long" ? "pos-side-long" : "pos-side-short";
|
const sideCls = side === "long" ? "pos-side-long" : "pos-side-short";
|
||||||
const mo = monitorOrder || {};
|
const mo = monitorOrder || {};
|
||||||
const cond = Array.isArray(pos.conditional_orders) ? pos.conditional_orders : [];
|
const cond = condOrdersFromPosition(pos);
|
||||||
const reg = Array.isArray(pos.regular_orders) ? pos.regular_orders : [];
|
const reg = Array.isArray(pos.regular_orders) ? pos.regular_orders : [];
|
||||||
const guess = guessTpslFromCondOrders(side, cond);
|
const guess = guessTpslFromCondOrders(side, cond);
|
||||||
const symAttr = esc(symbol).replace(/"/g, """);
|
const symAttr = esc(symbol).replace(/"/g, """);
|
||||||
@@ -583,7 +610,7 @@
|
|||||||
const symAttr = esc(x.symbol || "").replace(/"/g, """);
|
const symAttr = esc(x.symbol || "").replace(/"/g, """);
|
||||||
const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, """);
|
const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, """);
|
||||||
const contractsAttr = esc(String(x.contracts != null ? x.contracts : "")).replace(/"/g, """);
|
const contractsAttr = esc(String(x.contracts != null ? x.contracts : "")).replace(/"/g, """);
|
||||||
const cond = Array.isArray(x.conditional_orders) ? x.conditional_orders : [];
|
const cond = condOrdersFromPosition(x);
|
||||||
const reg = Array.isArray(x.regular_orders) ? x.regular_orders : [];
|
const reg = Array.isArray(x.regular_orders) ? x.regular_orders : [];
|
||||||
const guess = guessTpslFromCondOrders(x.side, cond);
|
const guess = guessTpslFromCondOrders(x.side, cond);
|
||||||
const slAttr = esc(String(guess.sl)).replace(/"/g, """);
|
const slAttr = esc(String(guess.sl)).replace(/"/g, """);
|
||||||
|
|||||||
@@ -109,6 +109,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
<script src="/assets/app.js?v=20260525-perf"></script>
|
<script src="/assets/app.js?v=20260525-okx-tpsl2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user