From 87910ed71a7e6ce7583dfb5c91523ba9170642cb Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 20 Aug 2026 14:33:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B8=81=E6=9C=AC=E4=BD=8D=E4=B9=B0=E5=B8=81?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E6=8C=89=E6=9C=80=E5=A4=A7=E5=8F=AF=E5=BC=80?= =?UTF-8?q?=E5=BC=A0=E6=95=B0=C3=97=E6=9D=83=E5=88=A9=E9=87=91=C3=97?= =?UTF-8?q?=E5=8F=AF=E9=85=8D=E7=BC=93=E5=86=B2=EF=BC=8C=E4=B8=8D=E5=85=A8?= =?UTF-8?q?=E9=A2=9D=E5=85=91=E6=8D=A2USDT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- crypto_monitor_okx/.env.example | 2 + docs/OKX单笔期权-币本位与USDT桥-开发方案.md | 3 + lib/common/static/options_panel.js | 12 +- lib/env/env_schema.py | 1 + lib/env/env_ui_manifest.py | 5 + lib/options/options_coin_open_lib.py | 69 +++++++-- lib/options/options_margin_mode_lib.py | 150 ++++++++++++++++++++ lib/options/options_register.py | 91 ++++++------ tests/test_options_margin_mode.py | 39 +++++ 9 files changed, 309 insertions(+), 63 deletions(-) diff --git a/crypto_monitor_okx/.env.example b/crypto_monitor_okx/.env.example index 01162bd..f1511f4 100644 --- a/crypto_monitor_okx/.env.example +++ b/crypto_monitor_okx/.env.example @@ -122,6 +122,8 @@ OKX_OPTIONS_COIN_COMPOUND=true OKX_OPTIONS_COIN_BUDGET_USDT=10 OKX_OPTIONS_COIN_MAX_USDT_ENABLED=false OKX_OPTIONS_COIN_MAX_USDT=50 +# 现货买入相对权利金缓冲:1.10=多买10%;也可写 0.10。按最大可开张数×权利金×缓冲买币,不全额兑换 +OKX_OPTIONS_COIN_SPOT_BUY_BUFFER=1.10 # 全仓复利:开启时隐藏单笔预算且不可用打满;关闭后恢复单笔预算 OKX_OPTIONS_COMPOUND_FULL_ENABLED=true OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED=false diff --git a/docs/OKX单笔期权-币本位与USDT桥-开发方案.md b/docs/OKX单笔期权-币本位与USDT桥-开发方案.md index 4167aa3..17925a8 100644 --- a/docs/OKX单笔期权-币本位与USDT桥-开发方案.md +++ b/docs/OKX单笔期权-币本位与USDT桥-开发方案.md @@ -57,6 +57,9 @@ | `OKX_OPTIONS_COIN_BUDGET_USDT` | 复利关闭时的固定 USDT 预算;或作展示参考 | `10` | | `OKX_OPTIONS_COIN_MAX_USDT_ENABLED` | 单笔不超过 N U 开关 | `false`(**默认关**) | | `OKX_OPTIONS_COIN_MAX_USDT` | 上限 N(仅开关开启时生效) | 如 `50`(可改) | +| `OKX_OPTIONS_COIN_SPOT_BUY_BUFFER` | 现货买入相对权利金倍数(也可写 `0.10`=+10%) | `1.10` | + +开仓买币:**先按预算估最大可开张数 → 买币 USDT ≈ 张数×卖一权利金×现货缓冲**,不全额把预算换成币。 说明: diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js index e25df2c..8802fbb 100644 --- a/lib/common/static/options_panel.js +++ b/lib/common/static/options_panel.js @@ -1344,8 +1344,11 @@ ? ((d && d.msg) || (sz && sz.msg) || "无法开仓") : "暂无卖一深度,无法开仓"; } else if (coinMode) { + const buyU = sz && sz.buy_usdt != null ? sz.buy_usdt : null; openBtn.textContent = - bud != null ? "买币并开仓(预算 ≈ " + Number(bud).toFixed(2) + " USDT)" : "买币并开仓 @ 卖一"; + buyU != null + ? ("买币并开仓(约 " + Number(buyU).toFixed(2) + " USDT)") + : (bud != null ? "买币并开仓(预算上限 ≈ " + Number(bud).toFixed(2) + " USDT)" : "买币并开仓 @ 卖一"); } else { openBtn.textContent = "限价买入 @ 卖一"; } @@ -1577,8 +1580,13 @@ ? ((latest && (latest.msg || (latest.sizing && latest.sizing.msg))) || "无法开仓") : "暂无卖一深度,无法开仓"; } else if (coinMode) { + const buyU = latest && latest.sizing && latest.sizing.buy_usdt != null + ? latest.sizing.buy_usdt + : null; btn.textContent = - bud != null ? "买币并开仓(预算 ≈ " + Number(bud).toFixed(2) + " USDT)" : "买币并开仓 @ 卖一"; + buyU != null + ? ("买币并开仓(约 " + Number(buyU).toFixed(2) + " USDT)") + : (bud != null ? "买币并开仓(预算上限 ≈ " + Number(bud).toFixed(2) + " USDT)" : "买币并开仓 @ 卖一"); } else { btn.textContent = "限价买入 @ 卖一"; } diff --git a/lib/env/env_schema.py b/lib/env/env_schema.py index 0898f42..cde5282 100644 --- a/lib/env/env_schema.py +++ b/lib/env/env_schema.py @@ -104,6 +104,7 @@ HOT_RELOAD_EXACT = frozenset({ "OKX_OPTIONS_COIN_BUDGET_USDT", "OKX_OPTIONS_COIN_MAX_USDT_ENABLED", "OKX_OPTIONS_COIN_MAX_USDT", + "OKX_OPTIONS_COIN_SPOT_BUY_BUFFER", "OKX_TRADE_MODE", "MAX_ACTIVE_HEDGE_PLANS", "HEDGE_PLAN_LIVE_ORDER", diff --git a/lib/env/env_ui_manifest.py b/lib/env/env_ui_manifest.py index f138174..7e8c5ac 100644 --- a/lib/env/env_ui_manifest.py +++ b/lib/env/env_ui_manifest.py @@ -174,6 +174,11 @@ _OPTIONS_SECTION: dict[str, Any] = { "币本位单笔上限(USDT)", "仅上限开关开启时生效", ), + ( + "OKX_OPTIONS_COIN_SPOT_BUY_BUFFER", + "币本位现货买入缓冲", + "相对权利金倍数,默认 1.10(=多买10%);也可写 0.10 表示+10%。按最大可开张数×卖一权利金×本缓冲买币,不全额兑换", + ), ( "OKX_OPTIONS_COMPOUND_FULL_ENABLED", "全仓复利开关", diff --git a/lib/options/options_coin_open_lib.py b/lib/options/options_coin_open_lib.py index f9923a9..48db734 100644 --- a/lib/options/options_coin_open_lib.py +++ b/lib/options/options_coin_open_lib.py @@ -17,6 +17,7 @@ from lib.options.options_margin_mode_lib import ( is_coin_margin_mode, margin_mode_from_inst_id, normalize_options_margin_mode, + plan_coin_open_by_budget, premium_ccy_for_mode, ) from lib.options.options_spot_bridge_lib import ( @@ -56,8 +57,9 @@ def open_coin_option_buy_full( target_index: float | None = None, profit_exit_enabled: bool = False, profit_exit_mult: float = 1.0, + target_sheets: int | None = None, ) -> dict[str, Any]: - """先买满 USDT 预算对应的币,再按卖一尽量开满.""" + """先按最大可开张数估权利金×现货缓冲买币,再开对应张数(不全额兑换预算).""" from lib.options.options_db import init_options_tables from lib.options.options_position_limit_lib import ( compound_full_single_position_block_msg, @@ -111,16 +113,56 @@ def open_coin_option_buy_full( "can_open": False, } - # 1) 买币(用买入前后可用差作为本轮币量,避免叠加原有现货) + ct_mult = float(q.get("ct_mult") or 0.01) + min_sz = int(q.get("min_sz") or 1) + idx = None + try: + idx = float(q.get("index_px") or q.get("idxPx") or 0) + except (TypeError, ValueError): + idx = 0.0 + if idx <= 0: + try: + from lib.exchange.okx_options_lib import fetch_index_price + + idx = float(fetch_index_price(ex, f"{underlying}-USD") or 0) + except Exception: + idx = 0.0 + + plan = plan_coin_open_by_budget( + quote_per_unit=float(ask), + ct_mult=ct_mult, + min_sz=min_sz, + budget_usdt=budget_usdt, + index_px=float(idx), + ask_sz=ask_sz, + target_sheets=target_sheets, + ) + if not plan.get("ok"): + return { + "ok": False, + "msg": plan.get("msg") or "无法规划买币张数", + "plan": plan, + "budget": budget_info, + "can_open": False, + } + buy_usdt = float(plan["buy_usdt"]) + sheets = int(plan["sheets"]) + + # 1) 仅买「权利金×现货缓冲」所需 USDT,不全额兑换预算 coin_before = fetch_trading_coin_available(ex, underlying) or 0.0 - buy = spot_market_buy_coin_with_usdt(ex, underlying=underlying, usdt_amount=budget_usdt) + buy = spot_market_buy_coin_with_usdt(ex, underlying=underlying, usdt_amount=buy_usdt) if not buy.get("ok"): - return {"ok": False, "msg": f"现货买入 {underlying} 失败: {buy.get('msg')}", "budget": budget_info} + return { + "ok": False, + "msg": f"现货买入 {underlying} 失败: {buy.get('msg')}", + "budget": budget_info, + "plan": plan, + } bridge_id = insert_bridge( conn, underlying=underlying, status=BRIDGE_BOUGHT, - budget_usdt=budget_usdt, + budget_usdt=buy_usdt, buy_ord_id=str(buy.get("ord_id") or ""), inst_id=inst_id, message="已买币,待开期权", @@ -143,6 +185,7 @@ def open_coin_option_buy_full( "msg": "买币后读不到可用余额,已尝试卖回 USDT", "rollback": rb, "budget": budget_info, + "plan": plan, } coin_bought = max(0.0, float(coin_after) - float(coin_before or 0)) if coin_bought <= 0: @@ -155,8 +198,6 @@ def open_coin_option_buy_full( return {"ok": False, "msg": "买币后可用增量无效", "rollback": rb, "budget": budget_info} update_bridge(conn, bridge_id, coin_bought=float(coin_bought)) - ct_mult = float(q.get("ct_mult") or 0.01) - min_sz = int(q.get("min_sz") or 1) sizing = calc_sheets_from_coin_balance( quote_per_unit=float(ask), ct_mult=ct_mult, @@ -172,9 +213,10 @@ def open_coin_option_buy_full( reason=sizing.get("msg") or "张数不足", coin_amount=float(coin_bought), ) - return {"ok": False, "msg": sizing.get("msg"), "sizing": sizing, "rollback": rb, "budget": budget_info} + return {"ok": False, "msg": sizing.get("msg"), "sizing": sizing, "rollback": rb, "budget": budget_info, "plan": plan} - sheets = int(sizing["sheets"]) + # 实盘以买到的币为准,但不超过规划张数 + sheets = min(int(sizing["sheets"]), int(plan["sheets"])) capped, cap_msg = cap_option_buy_sheets_to_ask_depth(sheets, ask_sz, min_sz=min_sz) if capped is None: rb = rollback_bought_coin_to_usdt( @@ -190,6 +232,13 @@ def open_coin_option_buy_full( "coin_premium": round(sheets * float(ask) * ct_mult, 8), "ask_depth_capped": True, } + else: + sizing = { + "ok": True, + "sheets": sheets, + "eth_amount": round(sheets * ct_mult, 8), + "coin_premium": round(sheets * float(ask) * ct_mult, 8), + } tick_sz = q.get("tick_sz") order = cfg["place_option_limit_order"]( @@ -292,7 +341,7 @@ def open_coin_option_buy_full( signal_note=signal_note, exchange_ord_id=ord_id, bridge_id=bridge_id, - budget_usdt=budget_usdt, + budget_usdt=buy_usdt, premium_ccy=premium_ccy, profit_exit_enabled=profit_exit_enabled, profit_exit_mult=profit_exit_mult, diff --git a/lib/options/options_margin_mode_lib.py b/lib/options/options_margin_mode_lib.py index 3e97f36..246bebb 100644 --- a/lib/options/options_margin_mode_lib.py +++ b/lib/options/options_margin_mode_lib.py @@ -134,6 +134,156 @@ def compute_coin_budget_usdt( } +def normalize_coin_spot_buy_buffer(raw: Any = None) -> float: + """ + 现货买入相对权利金的倍数缓冲. + env OKX_OPTIONS_COIN_SPOT_BUY_BUFFER 默认 1.10(=多买 10%). + 也可写 0.10 表示 +10%. + """ + if raw is None: + v = _env_float("OKX_OPTIONS_COIN_SPOT_BUY_BUFFER", 1.10) + else: + try: + v = float(raw) + except (TypeError, ValueError): + v = 1.10 + if v <= 0: + return 1.10 + if v < 1.0: + return 1.0 + v + return v + + +def plan_coin_open_by_budget( + *, + quote_per_unit: float, + ct_mult: float, + min_sz: int, + budget_usdt: float, + index_px: float, + ask_sz: float | None = None, + spot_buy_buffer: float | None = None, + target_sheets: int | None = None, +) -> dict[str, Any]: + """ + 先按预算/卖一估最大可开张数,再按「权利金 × 现货缓冲」算应买现货 USDT. + 不全额把预算换成币. + """ + import math + + from lib.exchange.okx_options_lib import cap_option_buy_sheets_to_ask_depth + + ask = float(quote_per_unit or 0) + mult = float(ct_mult or 0.01) + need = max(1, int(min_sz or 1)) + budget = max(0.0, float(budget_usdt or 0)) + idx = float(index_px or 0) + buf = normalize_coin_spot_buy_buffer(spot_buy_buffer) + if ask <= 0 or mult <= 0: + return {"ok": False, "msg": "卖一价无效", "sheets": 0, "buy_usdt": 0.0} + if idx <= 0: + return {"ok": False, "msg": "缺少指数价,无法估算买币 USDT", "sheets": 0, "buy_usdt": 0.0} + if budget <= 0: + return {"ok": False, "msg": "USDT 预算无效", "sheets": 0, "buy_usdt": 0.0} + + per_sheet_coin = ask * mult + # 每张开仓需买的币(含缓冲)及其约合 USDT + per_sheet_buy_coin = per_sheet_coin * buf + per_sheet_usdt = per_sheet_buy_coin * idx + if per_sheet_usdt <= 0: + return {"ok": False, "msg": "无法计算单张买币成本", "sheets": 0, "buy_usdt": 0.0} + + max_by_budget = int(math.floor((budget / per_sheet_usdt) + 1e-12)) + if target_sheets is not None: + try: + want = int(target_sheets) + except (TypeError, ValueError): + want = 0 + if want < need: + return { + "ok": False, + "msg": f"指定张数无效(需≥{need})", + "sheets": 0, + "buy_usdt": 0.0, + "max_by_budget": max_by_budget, + } + sheets = min(want, max_by_budget) + if sheets < want: + return { + "ok": False, + "msg": ( + f"预算约可开 {max_by_budget} 张(含现货缓冲×{buf:g})," + f"不足指定 {want} 张" + ), + "sheets": 0, + "buy_usdt": 0.0, + "max_by_budget": max_by_budget, + "spot_buy_buffer": buf, + } + else: + sheets = max_by_budget + + capped, cap_msg = cap_option_buy_sheets_to_ask_depth(sheets, ask_sz, min_sz=need) + ask_depth_capped = False + if capped is None: + return { + "ok": False, + "msg": cap_msg or "卖一深度不足", + "sheets": 0, + "buy_usdt": 0.0, + "spot_buy_buffer": buf, + } + if int(capped) < sheets: + sheets = int(capped) + ask_depth_capped = True + + if sheets < need: + return { + "ok": False, + "msg": ( + f"预算不足,无法买入 {need} 张" + f"(单张约需 {per_sheet_usdt:.4f} USDT,含现货缓冲×{buf:g})" + ), + "sheets": sheets, + "buy_usdt": 0.0, + "per_sheet_usdt": round(per_sheet_usdt, 8), + "spot_buy_buffer": buf, + "max_by_budget": max_by_budget, + } + + premium_coin = sheets * per_sheet_coin + buy_coin = premium_coin * buf + buy_usdt = min(budget, buy_coin * idx) + # 再保险:向下对齐,避免浮点导致略超预算 + buy_usdt = min(budget, round(buy_usdt, 8)) + out = { + "ok": True, + "msg": "" if not ask_depth_capped else (cap_msg or f"已按卖一深度限制为 {sheets} 张"), + "sheets": sheets, + "eth_amount": round(sheets * mult, 8), + "coin_premium": round(premium_coin, 8), + "total_premium": round(premium_coin, 8), + "per_sheet_coin": per_sheet_coin, + "buy_coin": round(buy_coin, 8), + "buy_usdt": round(buy_usdt, 8), + "budget_usdt": round(budget, 8), + "spot_buy_buffer": buf, + "index_px": idx, + "max_by_budget": max_by_budget, + "ask_depth_capped": ask_depth_capped, + "est_note": ( + f"按最大可开 {sheets} 张×卖一权利金×现货缓冲{buf:g}估买币;" + f"不全额兑换预算" + ), + } + if target_sheets is not None: + out["est_note"] = ( + f"指定 {sheets} 张×卖一权利金×现货缓冲{buf:g}估买币;不全额兑换" + ) + out["target_sheets"] = int(target_sheets) + return out + + def calc_sheets_from_coin_balance( *, quote_per_unit: float, diff --git a/lib/options/options_register.py b/lib/options/options_register.py index 93c9900..466f9e0 100644 --- a/lib/options/options_register.py +++ b/lib/options/options_register.py @@ -553,12 +553,11 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: # 币本位:报价预览走 USDT 预算→估币→张数,禁止再查 USDC try: from lib.options.options_margin_mode_lib import ( - calc_sheets_from_coin_balance, is_coin_margin_mode, margin_mode_from_inst_id, ) from lib.options.options_coin_open_lib import coin_budget_preview - from lib.exchange.okx_options_lib import cap_option_buy_sheets_to_ask_depth, option_buy_liquidity_ok + from lib.exchange.okx_options_lib import option_buy_liquidity_ok if is_coin_margin_mode(): ask = q.get("ask") @@ -637,61 +636,31 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: ) idx = _safe_float(q.get("index_px")) or _safe_float(q.get("idxPx")) budget_usdt = float(budget_info["budget_usdt"]) - est_coin = (budget_usdt / float(idx)) if idx and float(idx) > 0 else None - if est_coin is None or est_coin <= 0: - return jsonify( - { - **q, - "ok": True, - "can_open": False, - "msg": "无法用指数估算可买币量", - "options_margin_mode": "coin", - "coin_budget": budget_info, - "sizing": { - "ok": False, - "msg": "缺少指数价,无法预估张数", - "sheets": 0, - "eth_amount": 0.0, - "total_premium": 0.0, - }, - } - ) - sizing = calc_sheets_from_coin_balance( + target_sheets = sheet_count if mode == "sheets" and sheet_count is not None else None + if mode == "eth" and request.args.get("eth"): + # 指定币量:按币量反推张数后再走统一规划 + try: + eth_want = float(request.args.get("eth")) + except (TypeError, ValueError): + eth_want = 0.0 + if eth_want > 0 and float(ct_mult) > 0: + import math + + target_sheets = max(int(min_sz), int(math.floor(eth_want / float(ct_mult) + 1e-12))) + from lib.options.options_margin_mode_lib import plan_coin_open_by_budget + + sizing = plan_coin_open_by_budget( quote_per_unit=float(ask), ct_mult=float(ct_mult), min_sz=int(min_sz), - coin_available=float(est_coin), + budget_usdt=budget_usdt, + index_px=float(idx or 0), + ask_sz=ask_sz, + target_sheets=target_sheets, ) if sizing.get("ok"): - capped, cap_msg = cap_option_buy_sheets_to_ask_depth( - int(sizing.get("sheets") or 0), - ask_sz, - min_sz=int(min_sz), - ) - if capped is None: - sizing = { - "ok": False, - "msg": cap_msg, - "sheets": 0, - "eth_amount": 0.0, - "total_premium": 0.0, - } - elif capped < int(sizing.get("sheets") or 0): - sizing = calc_sheets_from_coin_balance( - quote_per_unit=float(ask), - ct_mult=float(ct_mult), - min_sz=int(min_sz), - coin_available=float(capped) * float(ask) * float(ct_mult), - ) - if sizing.get("ok"): - sizing["ask_depth_capped"] = True - sizing["ask_sz"] = ask_sz - sizing["msg"] = f"已按卖一深度限制为 {capped} 张" - if sizing.get("ok"): - sizing["total_premium"] = sizing.get("coin_premium") sizing["premium_ccy"] = (inst_id.split("-")[0] if inst_id else "ETH").upper() - sizing["est_coin"] = round(float(est_coin), 8) - sizing["est_note"] = "张数为预算估币预览;实盘按买币后可用量开满" + sizing["est_coin"] = sizing.get("buy_coin") q = _attach_close_preview( cfg, ex, @@ -1025,6 +994,25 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: from lib.options.options_coin_open_lib import open_coin_option_buy_full if is_coin_margin_mode(): + want_sheets = None + if mode == "sheets": + try: + want_sheets = int(data.get("sheets") or 0) or None + except (TypeError, ValueError): + want_sheets = None + elif mode == "eth": + try: + eth_want = float(data.get("eth") or 0) + except (TypeError, ValueError): + eth_want = 0.0 + if eth_want > 0: + q0 = cfg["quote_option_contract"](ex, inst_id) + ct0 = float((q0 or {}).get("ct_mult") or 0.01) + min0 = int((q0 or {}).get("min_sz") or 1) + if ct0 > 0: + import math + + want_sheets = max(min0, int(math.floor(eth_want / ct0 + 1e-12))) result = open_coin_option_buy_full( cfg, ex, @@ -1033,6 +1021,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: target_index=target_index, profit_exit_enabled=profit_exit_enabled, profit_exit_mult=profit_exit_mult, + target_sheets=want_sheets, ) if result.get("ok"): from lib.exchange.okx_options_lib import invalidate_option_positions_cache diff --git a/tests/test_options_margin_mode.py b/tests/test_options_margin_mode.py index 7de33e8..0f17cfe 100644 --- a/tests/test_options_margin_mode.py +++ b/tests/test_options_margin_mode.py @@ -82,6 +82,45 @@ class TestOptionsMarginMode(unittest.TestCase): self.assertTrue(r["ok"]) self.assertEqual(r["sheets"], 97) + def test_spot_buy_buffer_normalize(self): + from lib.options.options_margin_mode_lib import normalize_coin_spot_buy_buffer + + self.assertAlmostEqual(normalize_coin_spot_buy_buffer(1.10), 1.10) + self.assertAlmostEqual(normalize_coin_spot_buy_buffer(0.10), 1.10) + self.assertAlmostEqual(normalize_coin_spot_buy_buffer(1.25), 1.25) + + def test_plan_coin_open_by_budget(self): + from lib.options.options_margin_mode_lib import plan_coin_open_by_budget + + # ask 0.01, ct 0.1 → 单张权利金 0.001 ETH;×1.1=0.0011;×指数 2000 → 2.2 USDT/张 + r = plan_coin_open_by_budget( + quote_per_unit=0.01, + ct_mult=0.1, + min_sz=1, + budget_usdt=10.0, + index_px=2000.0, + ask_sz=100, + spot_buy_buffer=1.10, + ) + self.assertTrue(r["ok"], r.get("msg")) + self.assertEqual(r["sheets"], 4) # floor(10/2.2)=4 + self.assertAlmostEqual(r["buy_usdt"], 4 * 0.01 * 0.1 * 1.10 * 2000, places=4) + self.assertLess(r["buy_usdt"], 10.0) + + one = plan_coin_open_by_budget( + quote_per_unit=0.01, + ct_mult=0.1, + min_sz=1, + budget_usdt=10.0, + index_px=2000.0, + ask_sz=100, + spot_buy_buffer=1.10, + target_sheets=1, + ) + self.assertTrue(one["ok"], one.get("msg")) + self.assertEqual(one["sheets"], 1) + self.assertAlmostEqual(one["buy_usdt"], 0.01 * 0.1 * 1.10 * 2000, places=4) + if __name__ == "__main__": unittest.main()