xuanzhu 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1167,6 +1167,15 @@ btc_status → btc_sync → btc_signal(三周期打分 + 1h 择时)→ btc_b
1167
1167
  限定北京时间时段(例如只在 21–24 点评估)。**它只叫醒、不下单** ——
1168
1168
  下单仍由那一轮按三周期打分与风控判断。
1169
1169
 
1170
+ **后台巡检是无人值守的**:被监控/排期叫醒后,它会在**交易动作**上自主决策到底(真开仓、真平仓、
1171
+ 真撤单),不会停下来等确认。边界是代码强制的:清账本(`btc_paper_reset`)、资金划转
1172
+ (`btc_transfer_profit`)、写文件与执行命令仍会拒绝;硬性风控、三周期方向闸门、保证金与倍率校验
1173
+ 照常生效。
1174
+
1175
+ 监控不只盯价位:**行情获取本身也被监控**。后端每 5 分钟探一次实时价,K 线同步失败也计入同一个
1176
+ 「连续失败」计数(任一成功即清零);**连续 2 次失败就叫醒一轮巡检**去排查(代理 / 网络 / 限频 /
1177
+ 时间偏差),并把失败次数与最近错误写进提示词,修好前不得下单。同一轮故障只叫醒一次,恢复后计数清零。
1178
+
1170
1179
  组合条件用**受限公式**(`kind=formula`):`rsi_1h * 0.6 + adx_1h * 0.4`、
1171
1180
  `(close_1h - ma20_1h) / atr_1h`。变量是 `字段_周期`(+`price`),只允许四则/比较/`and or not`
1172
1181
  与 `abs/min/max`;求值在计算层按 AST 白名单执行,导入、属性、下标一律拒绝 ——
@@ -1251,7 +1260,7 @@ xzh btc stop # 两个都停(--paper 只停模拟盘)
1251
1260
  | 成交 | **限价挂单**:价格到达限价即成交;限价已在市场另一侧则**挂出即成交**,成交价取更优价(不会比限价差) |
1252
1261
  | 价格源 | 实时标记价;取不到才用库内最新收盘价,并在输出里标注「可能滞后」 |
1253
1262
  | 成本 | 手续费 0.05%(开平各一次),不计资金费 |
1254
- | 杠杆 | `leverage` 默认 1x:现金占用 = 名义 ÷ 杠杆,与实盘保证金口径一致 |
1263
+ | 杠杆 | **默认 9x**(`BTC_PAPER_LEVERAGE` 可覆盖),由 xzh 按可用余额与信心调整;余额不足时自动提到「够开这一单」,**上限 20x**,显式给的倍率不会被改写 |
1255
1264
  | 持仓 | 同时只允许一个持仓;可挂多档(最多 5 张未成交) |
1256
1265
  | 初始资金 | 默认 100000 USDT(`BTC_PAPER_CAPITAL` 可改) |
1257
1266
  | 账本 | `~/.btc/data/btc.db` 的 `paper_account` / `paper_trades` 表 |
@@ -14,11 +14,14 @@
14
14
  """
15
15
  from __future__ import annotations
16
16
 
17
+ import math
17
18
  import os
18
19
  import sqlite3
19
20
  from datetime import datetime, timedelta, timezone
20
21
 
21
22
  FEE_RATE = 0.0005
23
+ #: 余额不足时自动提高倍率的上限(与 Node 侧 `MAX_AUTO_LEVERAGE` 一致)
24
+ MAX_AUTO_LEVERAGE = 20.0
22
25
 
23
26
  SCHEMA = """
24
27
  CREATE TABLE IF NOT EXISTS paper_account (
@@ -369,6 +372,18 @@ def status(
369
372
  "unrealized_pct": round(unrealized / notional * 100, 2) if notional else 0.0,
370
373
  }
371
374
 
375
+ # 占用保证金 = 持仓占用 + 未成交挂单占用;「可用」就是 cash(已扣过占用)
376
+ pending_margin = 0.0
377
+ for row in conn.execute(
378
+ "SELECT notional, leverage FROM paper_orders WHERE status='open'"
379
+ ).fetchall():
380
+ pending_margin += float(row["notional"] or 0) / _lev(row["leverage"])
381
+ position_margin = (
382
+ float(account["entry_notional"]) / _lev(account["leverage"])
383
+ if account["side"]
384
+ else 0.0
385
+ )
386
+
372
387
  initial = float(account["initial_capital"])
373
388
  return {
374
389
  "symbol": symbol,
@@ -377,7 +392,10 @@ def status(
377
392
  "price_source": "mark" if _mark(mark) is not None else "close",
378
393
  "initial_capital": round(initial, 2),
379
394
  "cash": round(cash, 2),
395
+ "available": round(cash, 2),
380
396
  "equity": round(equity, 2),
397
+ "margin_used": round(position_margin + pending_margin, 2),
398
+ "leverage": _lev(account["leverage"]) if account["side"] else None,
381
399
  "return_pct": round((equity - initial) / initial * 100, 2) if initial else 0.0,
382
400
  "position": position,
383
401
  "open_orders": open_orders(conn),
@@ -424,7 +442,14 @@ def place_order(
424
442
  if pending is not None and int(pending["n"]) >= 5:
425
443
  raise RuntimeError("未成交挂单已有 5 张(多档上限),先撤掉不用的再挂")
426
444
 
427
- lev = _lev(leverage)
445
+ # 模拟盘是**合约**:没有倍率就没有意义。优先级:
446
+ # 调用方显式给 → BTC_PAPER_LEVERAGE → 默认 9(与内置策略的 leverage=9 一致)
447
+ explicit_leverage = leverage is not None
448
+ lev = (
449
+ _lev(leverage)
450
+ if explicit_leverage
451
+ else _lev(os.getenv("BTC_PAPER_LEVERAGE") or 9)
452
+ )
428
453
  qty = notional / limit_price
429
454
  margin = notional / lev
430
455
  fee = notional * FEE_RATE
@@ -436,10 +461,35 @@ def place_order(
436
461
  "SELECT notional, leverage FROM paper_orders WHERE status='open'"
437
462
  ).fetchall():
438
463
  occupied += float(row["notional"] or 0) / _lev(row["leverage"])
464
+
465
+ # 余额不足且倍率是默认值(不是调用方指定)时,允许自动提高到刚好够开,上限 20x。
466
+ # 显式指定 5x 说明调用方想保守,不能悄悄改成 20x。
467
+ auto_bumped = False
468
+ need = 0.0
469
+ room = cash - occupied - fee
470
+ if room > 0 and notional > 0:
471
+ need = math.ceil(notional / room)
472
+ if occupied + margin + fee > cash + 1e-9 and not explicit_leverage:
473
+ if room > 0 and need <= MAX_AUTO_LEVERAGE and need > lev:
474
+ lev = float(need)
475
+ margin = notional / lev
476
+ auto_bumped = True
477
+
439
478
  if occupied + margin + fee > cash + 1e-9:
479
+ need_text = (
480
+ f"至少需要 {need:g}x(上限 {MAX_AUTO_LEVERAGE:g}x)"
481
+ if need > 0
482
+ else "可用余额不足以覆盖已挂单占用与手续费"
483
+ )
484
+ if explicit_leverage:
485
+ raise RuntimeError(
486
+ f"可用保证金不足:按你指定的 {lev:g}x 本单需 {margin:.2f}"
487
+ f"(名义 {notional:.2f})+ 手续费 {fee:.2f},已挂单占用 {occupied:.2f},"
488
+ f"当前可用 {cash:.2f};{need_text},或减少名义金额。"
489
+ )
440
490
  raise RuntimeError(
441
- f"可用保证金不足:按 {lev:g}x 本单需 {margin:.2f}(名义 {notional:.2f})"
442
- f" + 手续费 {fee:.2f},已挂单占用 {occupied:.2f},当前可用 {cash:.2f}"
491
+ f"可用保证金不足:{need_text}。名义 {notional:.2f}、手续费 {fee:.2f}、"
492
+ f"已挂单占用 {occupied:.2f}、当前可用 {cash:.2f} —— 请减少名义或先入金。"
443
493
  )
444
494
 
445
495
  minutes = 60.0
@@ -469,6 +519,8 @@ def place_order(
469
519
  "status": "open",
470
520
  "expires_at": expires_at,
471
521
  "leverage": lev,
522
+ # 余额不足被自动提上来的倍率(调用方要据此提醒「强平更近」)
523
+ "leverage_auto": auto_bumped,
472
524
  }
473
525
 
474
526