feat中控条件单可指令实例账户内划转,Bridge 暴露划转接口。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -400,6 +400,31 @@ def register_hub_bridge_routes(app: Flask, app_module: Any) -> None:
|
|||||||
out["version"] = 1
|
out["version"] = 1
|
||||||
return jsonify(out)
|
return jsonify(out)
|
||||||
|
|
||||||
|
@app.route("/api/hub/options/transfer", methods=["POST"])
|
||||||
|
def api_hub_options_transfer():
|
||||||
|
"""中控指令:实例内资金户↔交易户划转(默认资金→交易,USDT)."""
|
||||||
|
if not hub_bridge_token_ok(request.headers):
|
||||||
|
return jsonify({"ok": False, "msg": "未授权", "version": 1}), 401
|
||||||
|
hub_ctx = app.config.get("HUB_CTX") or {}
|
||||||
|
cfg = hub_ctx.get("options_cfg")
|
||||||
|
if not isinstance(cfg, dict):
|
||||||
|
return jsonify({"ok": False, "msg": "期权未就绪", "version": 1}), 503
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
from lib.options.options_transfer_exec_lib import execute_options_internal_transfer
|
||||||
|
|
||||||
|
out = execute_options_internal_transfer(
|
||||||
|
cfg,
|
||||||
|
amount=data.get("amount"),
|
||||||
|
from_acct=(data.get("from") or "funding"),
|
||||||
|
to_acct=(data.get("to") or "trading"),
|
||||||
|
ccy=(data.get("ccy") or "USDT"),
|
||||||
|
)
|
||||||
|
if not isinstance(out, dict):
|
||||||
|
return jsonify({"ok": False, "msg": "划转返回异常", "version": 1}), 500
|
||||||
|
body = dict(out)
|
||||||
|
body["version"] = 1
|
||||||
|
return jsonify(body)
|
||||||
|
|
||||||
@app.route("/api/hub/options/cond_orders", methods=["GET"])
|
@app.route("/api/hub/options/cond_orders", methods=["GET"])
|
||||||
def api_hub_options_cond_orders_list():
|
def api_hub_options_cond_orders_list():
|
||||||
if not hub_bridge_token_ok(request.headers):
|
if not hub_bridge_token_ok(request.headers):
|
||||||
|
|||||||
@@ -158,6 +158,8 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
|
|||||||
"open_bridges": open_bridges,
|
"open_bridges": open_bridges,
|
||||||
"cond_order_locked": False,
|
"cond_order_locked": False,
|
||||||
"cond_orders_active": [],
|
"cond_orders_active": [],
|
||||||
|
"transfer_blocked": False,
|
||||||
|
"transfer_block_msg": "",
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
from lib.options.options_cond_order_lib import has_active_cond_order, list_cond_orders
|
from lib.options.options_cond_order_lib import has_active_cond_order, list_cond_orders
|
||||||
@@ -173,6 +175,19 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
|
|||||||
conn_c.close()
|
conn_c.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
|
from lib.options.options_account_pnl_lib import transfer_blocked_by_position_msg
|
||||||
|
|
||||||
|
conn_tb = cfg["get_db"]()
|
||||||
|
try:
|
||||||
|
tmsg = transfer_blocked_by_position_msg(conn_tb, ex)
|
||||||
|
if tmsg:
|
||||||
|
out["transfer_blocked"] = True
|
||||||
|
out["transfer_block_msg"] = tmsg
|
||||||
|
finally:
|
||||||
|
conn_tb.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return out
|
return out
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"ok": False, "enabled": True, "msg": str(e)}
|
return {"ok": False, "enabled": True, "msg": str(e)}
|
||||||
|
|||||||
@@ -1261,52 +1261,21 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
ex, err = _require_options_ex(cfg)
|
ex, err = _require_options_ex(cfg)
|
||||||
if ex is None:
|
if ex is None:
|
||||||
return jsonify({"ok": False, "msg": err})
|
return jsonify({"ok": False, "msg": err})
|
||||||
# 持仓/待卖回禁止划转(规格)
|
del ex # 执行层自行取 exchange_options
|
||||||
conn_blk = cfg["get_db"]()
|
|
||||||
try:
|
|
||||||
from lib.options.options_account_pnl_lib import transfer_blocked_by_position_msg
|
|
||||||
|
|
||||||
block = transfer_blocked_by_position_msg(conn_blk, ex)
|
|
||||||
if block:
|
|
||||||
return jsonify({"ok": False, "msg": block, "transfer_blocked": True})
|
|
||||||
finally:
|
|
||||||
conn_blk.close()
|
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
ccy = (data.get("ccy") or "USDC").upper()
|
from lib.options.options_transfer_exec_lib import execute_options_internal_transfer
|
||||||
from_acct = (data.get("from") or "funding").strip()
|
|
||||||
to_acct = (data.get("to") or "trading").strip()
|
|
||||||
try:
|
|
||||||
amount = float(data.get("amount"))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return jsonify({"ok": False, "msg": "数量无效"})
|
|
||||||
try:
|
|
||||||
import lib.exchange.okx_options_lib as _opt_lib
|
|
||||||
|
|
||||||
xfer = getattr(_opt_lib, "transfer_ccy", None)
|
# 前端传 ccy;缺省 USDT(币本位)
|
||||||
except Exception:
|
ccy = (data.get("ccy") or "USDT").upper()
|
||||||
xfer = None
|
return jsonify(
|
||||||
if not callable(xfer):
|
execute_options_internal_transfer(
|
||||||
xfer = cfg.get("transfer_ccy")
|
cfg,
|
||||||
result = xfer(ex, ccy, amount, from_acct, to_acct) if callable(xfer) else {
|
amount=data.get("amount"),
|
||||||
"ok": False,
|
from_acct=(data.get("from") or "funding"),
|
||||||
"msg": "划转不可用",
|
to_acct=(data.get("to") or "trading"),
|
||||||
}
|
ccy=ccy,
|
||||||
if result.get("ok"):
|
)
|
||||||
conn = cfg["get_db"]()
|
)
|
||||||
try:
|
|
||||||
init_options_tables(conn)
|
|
||||||
conn.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO options_transfer_log (ccy, amount, from_account, to_account, status, message)
|
|
||||||
VALUES (?, ?, ?, ?, 'ok', '')
|
|
||||||
""",
|
|
||||||
(ccy, amount, from_acct, to_acct),
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
_mark_balances_stale(cfg)
|
|
||||||
return jsonify(result)
|
|
||||||
|
|
||||||
@app.route("/api/options/spot/swap", methods=["POST"])
|
@app.route("/api/options/spot/swap", methods=["POST"])
|
||||||
@lr
|
@lr
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
"""期权账户内划转执行(本机设置 / 中控 Bridge 共用)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_float(v: Any) -> float | None:
|
||||||
|
if v is None or v == "":
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(v)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _pick_stable(bal: dict[str, Any] | None, *, account: str, ccy: str) -> float | None:
|
||||||
|
if not isinstance(bal, dict):
|
||||||
|
return None
|
||||||
|
acct = "trading" if account == "trading" else "funding"
|
||||||
|
ccy_l = (ccy or "usdt").strip().lower() or "usdt"
|
||||||
|
avail = _safe_float(bal.get(f"{acct}_{ccy_l}_avail"))
|
||||||
|
if avail is not None:
|
||||||
|
return round(float(avail), 8)
|
||||||
|
total = _safe_float(bal.get(f"{acct}_{ccy_l}"))
|
||||||
|
if total is not None:
|
||||||
|
return round(float(total), 8)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def execute_options_internal_transfer(
|
||||||
|
cfg: dict[str, Any],
|
||||||
|
*,
|
||||||
|
amount: Any,
|
||||||
|
from_acct: str = "funding",
|
||||||
|
to_acct: str = "trading",
|
||||||
|
ccy: str = "USDT",
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""资金户↔交易户划转;持仓/待卖回拒绝.成功后附带最新 USDT 余额."""
|
||||||
|
from lib.options.options_account_pnl_lib import transfer_blocked_by_position_msg
|
||||||
|
from lib.options.options_db import init_options_tables
|
||||||
|
|
||||||
|
ex = cfg.get("exchange_options")
|
||||||
|
ready_fn = cfg.get("options_api_ready")
|
||||||
|
if callable(ready_fn):
|
||||||
|
ok, reason = ready_fn(ex)
|
||||||
|
if not ok:
|
||||||
|
return {"ok": False, "msg": reason or "期权 API 未配置"}
|
||||||
|
if ex is None:
|
||||||
|
return {"ok": False, "msg": "交易所未就绪"}
|
||||||
|
|
||||||
|
src = (from_acct or "funding").strip().lower()
|
||||||
|
dst = (to_acct or "trading").strip().lower()
|
||||||
|
if src not in ("funding", "trading") or dst not in ("funding", "trading"):
|
||||||
|
return {"ok": False, "msg": "仅支持 funding/trading"}
|
||||||
|
if src == dst:
|
||||||
|
return {"ok": False, "msg": "转出与转入账户不能相同"}
|
||||||
|
ccy_u = (ccy or "USDT").strip().upper() or "USDT"
|
||||||
|
if ccy_u not in ("USDT", "USDC"):
|
||||||
|
return {"ok": False, "msg": "仅支持 USDT/USDC"}
|
||||||
|
amt = _safe_float(amount)
|
||||||
|
if amt is None or amt <= 0:
|
||||||
|
return {"ok": False, "msg": "数量无效"}
|
||||||
|
|
||||||
|
get_db = cfg.get("get_db")
|
||||||
|
if not callable(get_db):
|
||||||
|
return {"ok": False, "msg": "数据库未就绪"}
|
||||||
|
conn = get_db()
|
||||||
|
try:
|
||||||
|
block = transfer_blocked_by_position_msg(conn, ex)
|
||||||
|
if block:
|
||||||
|
return {"ok": False, "msg": block, "transfer_blocked": True}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
xfer = cfg.get("transfer_ccy")
|
||||||
|
if not callable(xfer):
|
||||||
|
try:
|
||||||
|
import lib.exchange.okx_options_lib as _opt_lib
|
||||||
|
|
||||||
|
xfer = getattr(_opt_lib, "transfer_ccy", None)
|
||||||
|
except Exception:
|
||||||
|
xfer = None
|
||||||
|
if not callable(xfer):
|
||||||
|
return {"ok": False, "msg": "划转不可用"}
|
||||||
|
|
||||||
|
result = xfer(ex, ccy_u, float(amt), src, dst)
|
||||||
|
if not isinstance(result, dict):
|
||||||
|
return {"ok": False, "msg": "划转返回异常"}
|
||||||
|
out = dict(result)
|
||||||
|
if not out.get("ok"):
|
||||||
|
out.setdefault("msg", "划转失败")
|
||||||
|
return out
|
||||||
|
|
||||||
|
conn2 = get_db()
|
||||||
|
try:
|
||||||
|
init_options_tables(conn2)
|
||||||
|
conn2.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO options_transfer_log (ccy, amount, from_account, to_account, status, message)
|
||||||
|
VALUES (?, ?, ?, ?, 'ok', ?)
|
||||||
|
""",
|
||||||
|
(ccy_u, float(amt), src, dst, ""),
|
||||||
|
)
|
||||||
|
conn2.commit()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
conn2.rollback()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
conn2.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
from lib.exchange.okx_options_lib import invalidate_options_balance_cache
|
||||||
|
from lib.instance.instance_live_push_lib import notify_instance_balance_changed
|
||||||
|
|
||||||
|
invalidate_options_balance_cache()
|
||||||
|
app_mod = cfg.get("app_module")
|
||||||
|
if app_mod is not None and hasattr(app_mod, "invalidate_account_balance_cache"):
|
||||||
|
app_mod.invalidate_account_balance_cache()
|
||||||
|
notify_instance_balance_changed()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
bal = {}
|
||||||
|
try:
|
||||||
|
fetch_bal = cfg.get("fetch_options_balances")
|
||||||
|
if callable(fetch_bal):
|
||||||
|
bal = fetch_bal(ex) or {}
|
||||||
|
except Exception:
|
||||||
|
bal = {}
|
||||||
|
trading_usdt = _pick_stable(bal, account="trading", ccy="USDT")
|
||||||
|
funding_usdt = _pick_stable(bal, account="funding", ccy="USDT")
|
||||||
|
out.update(
|
||||||
|
{
|
||||||
|
"ok": True,
|
||||||
|
"msg": out.get("msg") or "划转成功",
|
||||||
|
"ccy": ccy_u,
|
||||||
|
"amount": round(float(amt), 8),
|
||||||
|
"from": src,
|
||||||
|
"to": dst,
|
||||||
|
"trading_usdt": trading_usdt,
|
||||||
|
"funding_usdt": funding_usdt,
|
||||||
|
"transfer_blocked": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return out
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
"""账户内划转执行:门禁与余额回写."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
|
||||||
|
class TestOptionsTransferExec(unittest.TestCase):
|
||||||
|
def test_block_when_position(self):
|
||||||
|
from lib.options.options_transfer_exec_lib import execute_options_internal_transfer
|
||||||
|
|
||||||
|
conn = MagicMock()
|
||||||
|
cfg = {
|
||||||
|
"exchange_options": object(),
|
||||||
|
"options_api_ready": lambda ex: (True, ""),
|
||||||
|
"get_db": lambda: conn,
|
||||||
|
"transfer_ccy": MagicMock(),
|
||||||
|
}
|
||||||
|
with patch(
|
||||||
|
"lib.options.options_transfer_exec_lib.transfer_blocked_by_position_msg",
|
||||||
|
create=True,
|
||||||
|
):
|
||||||
|
with patch(
|
||||||
|
"lib.options.options_account_pnl_lib.transfer_blocked_by_position_msg",
|
||||||
|
return_value="期权持仓中禁止划转",
|
||||||
|
):
|
||||||
|
out = execute_options_internal_transfer(
|
||||||
|
cfg, amount=10, from_acct="funding", to_acct="trading"
|
||||||
|
)
|
||||||
|
self.assertFalse(out["ok"])
|
||||||
|
self.assertTrue(out.get("transfer_blocked"))
|
||||||
|
cfg["transfer_ccy"].assert_not_called()
|
||||||
|
|
||||||
|
def test_success_returns_balances(self):
|
||||||
|
from lib.options.options_transfer_exec_lib import execute_options_internal_transfer
|
||||||
|
|
||||||
|
conn = MagicMock()
|
||||||
|
xfer = MagicMock(return_value={"ok": True, "msg": "ok"})
|
||||||
|
fetch_bal = MagicMock(
|
||||||
|
return_value={
|
||||||
|
"trading_usdt": 30.0,
|
||||||
|
"funding_usdt": 70.0,
|
||||||
|
"trading_usdt_avail": 30.0,
|
||||||
|
"funding_usdt_avail": 70.0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cfg = {
|
||||||
|
"exchange_options": object(),
|
||||||
|
"options_api_ready": lambda ex: (True, ""),
|
||||||
|
"get_db": lambda: conn,
|
||||||
|
"transfer_ccy": xfer,
|
||||||
|
"fetch_options_balances": fetch_bal,
|
||||||
|
}
|
||||||
|
with patch(
|
||||||
|
"lib.options.options_account_pnl_lib.transfer_blocked_by_position_msg",
|
||||||
|
return_value=None,
|
||||||
|
), patch(
|
||||||
|
"lib.options.options_db.init_options_tables",
|
||||||
|
), patch(
|
||||||
|
"lib.exchange.okx_options_lib.invalidate_options_balance_cache",
|
||||||
|
), patch(
|
||||||
|
"lib.instance.instance_live_push_lib.notify_instance_balance_changed",
|
||||||
|
):
|
||||||
|
out = execute_options_internal_transfer(
|
||||||
|
cfg, amount=10, from_acct="funding", to_acct="trading", ccy="USDT"
|
||||||
|
)
|
||||||
|
self.assertTrue(out["ok"])
|
||||||
|
self.assertAlmostEqual(out["trading_usdt"], 30.0)
|
||||||
|
self.assertAlmostEqual(out["funding_usdt"], 70.0)
|
||||||
|
xfer.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user