xuanzhu 0.9.0 → 0.9.2

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
@@ -168,7 +168,7 @@ xzh
168
168
  | `xzh btc` | 进入 BTC 量化交易模式:工作目录固定 `~/.btc`(不可 `/switch`),只加载交易相关技能 |
169
169
  | `xzh btc moni` | 进入 **BTC 模拟盘**:虚拟账本开平仓,独立后端,不涉及真钱 |
170
170
  | `xzh btc apikey` | 管理交易所 API 凭据(录入 / 更新 / 删除 / 查看别名);首次 `xzh btc` 也会自动检测 |
171
- | `xzh btc fk` | 硬性风控阈值:持仓亏损(默认 50%)/ 当日亏损 / 连亏次数 |
171
+ | `xzh btc fk` | 硬性风控阈值:持仓亏损(默认 50%)/ 当日亏损(10%)/ 连亏次数(3)——**三条都真实生效** |
172
172
  | `xzh btc platform` | 选择交易平台(默认币安;交互式,也可 `xzh btc platform binance`) |
173
173
  | `xzh btc setup` | 准备 btc 计算环境:释放脚本 + 建 `~/.btc/venv` + 安装 numpy / pandas(首次进入时自动执行) |
174
174
  | `xzh btc stop` | 停止 btc 常驻后端(默认观察 + 模拟盘;`--paper` 只停模拟盘。退出界面**不会**停它) |
@@ -1128,7 +1128,7 @@ btc_status → btc_sync → btc_signal(三阶段)→ btc_backtest(可选
1128
1128
  | `btc_price` | 币安永续最新价、24h 涨跌与成交额(实时,不落库) |
1129
1129
  | `btc_status` | 计算环境是否就绪、日线/小时线根数与最新时间、风控参数 |
1130
1130
  | `btc_sync` | 从币安增量同步 K 线到 `~/.btc/data/btc.db`(走 xzh 的全局代理) |
1131
- | `btc_signal` | 三阶段买卖点(周线定势 → 日线定向 → 小时线定时) |
1131
+ | `btc_signal` | 三阶段买卖点(周线定势 → 日线定向 → 4 小时线定时) |
1132
1132
  | `btc_backtest` | 内置融合策略全量回测,`overrides` 可临时覆盖参数做对比 |
1133
1133
  | `btc_schedule` | 安排巡检:默认**改写下一跳**(旧的自动取消),`mode=add` 追加额外检查点 |
1134
1134
  | `btc_unschedule` | 撤销巡检计划(按 id,或 `scope`:`next` / `extra` / `all`) |
@@ -1164,17 +1164,20 @@ btc_status → btc_sync → btc_signal(三阶段)→ btc_backtest(可选
1164
1164
  `xzh btc` 会确保一个**独立的后端进程**在跑,它按**模型自己决定的间隔**巡检:
1165
1165
 
1166
1166
  - 每轮巡检会取行情、算三阶段、给结论,然后自己决定下次什么时候再看
1167
- —— 例如判断「小时线接近触发,45 分钟后复核」;
1167
+ —— 例如判断「4 小时线接近触发,45 分钟后复核」;
1168
1168
  - 计划写进一个**排期队列**(`~/.btc/run/schedules.json`),后台每 60 秒检查一次,
1169
1169
  到点就执行 —— 不是 sleep,所以你随时能让它多排一次检查,不用等它「睡醒」;
1170
1170
  - 间隔被限制在 **10 分钟到 12 小时**之间:太短会连续调模型烧钱,太长会错过行情;
1171
1171
  - **关键时点由代码兜底**,不依赖模型记性:日线收盘(北京 08:00)、资金费结算
1172
- (北京 00:00 / 08:00 / 16:00)、有持仓时的小时线收盘、以及价格较上次巡检波动 ≥2.5%
1172
+ (北京 00:00 / 08:00 / 16:00)、有持仓时的 4 小时线收盘、以及价格较上次巡检波动 ≥2.5%
1173
1173
  都会额外触发一轮巡检(波动触发 10 分钟内不重复);
1174
1174
  - **改期 / 取消**:直接说「30 分钟后再看」,agent 用 `btc_schedule` 改写下一跳(原计划自动取消);
1175
1175
  说「取消这次巡检」用 `btc_unschedule`;`xzh btc status` 会列出每条计划的短 id 供精确撤销;
1176
1176
  - **退出界面不会停它**,下次进入复用同一个进程(不会重复启动);`xzh btc stop` 才停;
1177
1177
  - `xzh btc status` 看最近结论、队列条数、下一条计划与理由(后端停了也会显示待执行计划);
1178
+ - **每轮巡检先核对上次动作**:上一轮挂的限价单成交了吗?成交后止损挂上了吗?
1179
+ 平仓到底平掉没有?这些由代码在巡检开始前核对(`run/actions.jsonl` 账本),
1180
+ 发现「持仓无止损」「距强平 <5%」「挂单堆积」会直接写进本轮提示词并要求优先处理;
1178
1181
  - **过程实时可见**:巡检的思考、工具调用与结论会以 `[后台 15:04] 思考:…` 的形式
1179
1182
  出现在对话输出区(每 2.5 秒拉一次增量);刷屏时用 `/feed` 关掉,再看一次 `/feed` 打开;
1180
1183
  - 日志:`~/.btc/logs/backend.log`(进程)、`~/.btc/logs/inspect.log`(每轮结论,可复盘);
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env python3
2
- """三阶段买卖点判断:周线定势 → 日线定向 → 小时线定时。
2
+ """三阶段买卖点判断:周线定势 → 日线定向 → 4 小时线定时。
3
3
 
4
4
  设计原则:
5
5
  - 只做多周期一致性判断,冲突时明确 stand_aside,不硬凑信号。
6
6
  - 所有指标来自 `core.bt_engine.indicators`(与回测/实盘同一套口径,因果无未来函数)。
7
- - 周线由本地日线重采样;小时线只读本地 1h(行情同步由调用方负责),缺失则降级并标注。
7
+ - 周线由日线重采样;4 小时线由 1h 重采样(行情同步只维护 1d + 1h),缺失则降级并标注。
8
8
 
9
9
  对外接口:
10
10
  - `analyze_timeframe(timeframe, limit)`:单周期指标摘要(1w / 1d / 4h / 1h)
@@ -50,7 +50,7 @@ def _hourly(limit: int = 400) -> tuple[pd.DataFrame | None, str]:
50
50
  """返回 (DataFrame, 来源)。来源:local / unavailable:<原因>。
51
51
 
52
52
  刻意**不联网**:行情同步由调用方(xzh 侧)负责并落库,这里只读本地 1h。
53
- 缺失时返回 degraded,让结论里明确写「小时线未确认」——
53
+ 缺失时返回 degraded,让结论里明确写「4 小时线未确认」——
54
54
  否则三阶段会在一次工具调用里卡住等网络,而且模型分不清「没数据」和「网络差」。
55
55
  """
56
56
  from core.bt_engine import load_data
@@ -64,6 +64,29 @@ def _hourly(limit: int = 400) -> tuple[pd.DataFrame | None, str]:
64
64
  return None, f"unavailable: {type(e).__name__}: {e}"
65
65
 
66
66
 
67
+ def _h4(limit: int = 360) -> tuple[pd.DataFrame | None, str]:
68
+ """4 小时线:由本地 1h **重采样**得到。
69
+
70
+ 为什么不单独同步 4h:多一个周期就多一份要维护、要对齐、要补的数据,
71
+ 而 4h 完全由 1h 聚合而来(`o=first / h=max / l=min / c=last / v=sum`)。
72
+ 同步链路因此不用改(仍是 1d + 1h)。
73
+ """
74
+ need = max(240, int(limit) * 4)
75
+ raw, src = _hourly(limit=need)
76
+ if raw is None:
77
+ return None, src.replace("1h", "4h")
78
+ h4 = (
79
+ raw.set_index("ts")
80
+ .resample("4h")
81
+ .agg({"o": "first", "h": "max", "l": "min", "c": "last", "v": "sum"})
82
+ .dropna()
83
+ .reset_index()
84
+ )
85
+ if len(h4) < 120:
86
+ return None, f"unavailable: 重采样后只有 {len(h4)} 根 4h(<120),先同步更多 1h 数据"
87
+ return h4.tail(limit).reset_index(drop=True), f"{src}→4h"
88
+
89
+
67
90
  def _ind(df: pd.DataFrame) -> pd.DataFrame:
68
91
  from core.bt_engine import indicators
69
92
  return indicators(df)
@@ -248,13 +271,18 @@ def _stage2_daily(d: pd.DataFrame, trend: str) -> dict:
248
271
  }
249
272
 
250
273
 
251
- def _stage3_hourly(d: pd.DataFrame | None, source: str, bias: str) -> dict:
274
+ def _stage3_4h(d: pd.DataFrame | None, source: str, bias: str) -> dict:
275
+ """第三阶段:4 小时线定时。
276
+
277
+ 原来用 1h:周(7天) → 日(1天) → 小时线(24倍) 跨度过大,最噪音的周期反而有否决权。
278
+ 换成 4h 后是 7 天 → 1 天 → 4 小时,相邻约 6–7 倍,且 4h 一根 K 线的噪音明显低于 1h。
279
+ """
252
280
  if bias == "wait":
253
281
  return {"entry": "wait", "source": source,
254
- "reason": "日线无方向,小时线不单独入场"}
282
+ "reason": "日线无方向,4 小时线不单独入场"}
255
283
  if d is None:
256
284
  return {"entry": "wait", "source": source, "degraded": True,
257
- "reason": f"小时线不可用({source}):只做日线级判断,等下根日线收盘或补 1h 数据后再定时"}
285
+ "reason": f"4 小时线不可用({source}):只做日线级判断,等下根 4h 收盘或补 1h 数据后再定时"}
258
286
 
259
287
  last = d.iloc[-1]
260
288
  prev = d.iloc[-2] if len(d) >= 2 else last
@@ -263,13 +291,14 @@ def _stage3_hourly(d: pd.DataFrame | None, source: str, bias: str) -> dict:
263
291
  rsi = _num(last.get("rsi"))
264
292
  hist = _num(last.get("macd_hist"))
265
293
  atr = _num(last.get("atr"))
266
- lows = d["l"].tail(24)
267
- highs = d["h"].tail(24)
294
+ # 近 24 小时 = 6 根 4h(4h 周期下不能再用 24 根,那是 4 天)
295
+ lows = d["l"].tail(6)
296
+ highs = d["h"].tail(6)
268
297
 
269
298
  if bias == "long":
270
299
  checks = {
271
- "站上 MA20H": c > ma20 > 0,
272
- "MA20H > MA50H": ma20 > ma50 > 0,
300
+ "站上 MA20(4h)": c > ma20 > 0,
301
+ "MA20 > MA50(4h)": ma20 > ma50 > 0,
273
302
  "RSI 45–72(不追超买)": 45 <= rsi <= 72,
274
303
  "MACD 柱 > 0": hist > 0,
275
304
  }
@@ -291,8 +320,8 @@ def _stage3_hourly(d: pd.DataFrame | None, source: str, bias: str) -> dict:
291
320
  }
292
321
 
293
322
  checks = {
294
- "跌破 MA20H": c < ma20 > 0,
295
- "MA20H < MA50H": ma20 < ma50 > 0,
323
+ "跌破 MA20(4h)": c < ma20 > 0,
324
+ "MA20 < MA50(4h)": ma20 < ma50 > 0,
296
325
  "RSI 28–55(不追超卖)": 28 <= rsi <= 55,
297
326
  "MACD 柱 < 0": hist < 0,
298
327
  }
@@ -318,14 +347,14 @@ def _decide(trend: str, bias: str, entry: str) -> tuple[str, str, str]:
318
347
  """返回 (action, 中文说明, 仓位建议)。"""
319
348
  if trend == "bull" and bias == "long":
320
349
  if entry == "now":
321
- return "long", "周线多头 + 日线做多 + 小时线触发:顺势做多", "标准仓位(1.0x)"
322
- return "wait", "周线多头、日线做多,但小时线未触发:等回踩 MA20H/前低再进", "0(等待)"
350
+ return "long", "周线多头 + 日线做多 + 4 小时线触发:顺势做多", "标准仓位(1.0x)"
351
+ return "wait", "周线多头、日线做多,但 4 小时线未触发:等回踩 MA20(4h)/前低再进", "0(等待)"
323
352
  if trend == "bear" and bias == "short":
324
353
  if entry == "now":
325
- return "short", "周线空头 + 日线做空 + 小时线触发:顺势做空", "标准仓位(1.0x)"
326
- return "wait", "周线空头、日线做空,但小时线未触发:等反弹到 MA20H 再进", "0(等待)"
354
+ return "short", "周线空头 + 日线做空 + 4 小时线触发:顺势做空", "标准仓位(1.0x)"
355
+ return "wait", "周线空头、日线做空,但 4 小时线未触发:等反弹到 MA20(4h) 再进", "0(等待)"
327
356
  if trend == "range" and bias in ("long", "short") and entry == "now":
328
- return bias, "震荡市边缘反转 + 小时线确认:轻仓逆势", "半仓(0.5x),到区间中部止盈"
357
+ return bias, "震荡市边缘反转 + 4 小时线确认:轻仓逆势", "半仓(0.5x),到区间中部止盈"
329
358
  if trend == "range":
330
359
  return "stand_aside", "周线震荡且未到区间边缘:不做", "0"
331
360
  if bias == "wait":
@@ -334,7 +363,7 @@ def _decide(trend: str, bias: str, entry: str) -> tuple[str, str, str]:
334
363
 
335
364
 
336
365
  def three_stage_signal(symbol: str | None = None) -> dict:
337
- """三阶段买卖点判断:周线定势 → 日线定向 → 小时线定时。"""
366
+ """三阶段买卖点判断:周线定势 → 日线定向 → 4 小时线定时。"""
338
367
  try:
339
368
  daily = _daily()
340
369
  except Exception as e:
@@ -348,8 +377,8 @@ def three_stage_signal(symbol: str | None = None) -> dict:
348
377
  s1 = _stage1_weekly(weekly_ind)
349
378
  s2 = _stage2_daily(daily_ind, s1["trend"])
350
379
 
351
- hourly_df, hourly_src = _hourly(400)
352
- s3 = _stage3_hourly(_ind(hourly_df) if hourly_df is not None else None, hourly_src, s2["bias"])
380
+ h4_df, h4_src = _h4(360)
381
+ s3 = _stage3_4h(_ind(h4_df) if h4_df is not None else None, h4_src, s2["bias"])
353
382
 
354
383
  action, reason, position = _decide(s1["trend"], s2["bias"], s3.get("entry", "wait"))
355
384
  confidence = round(min(s1["confidence"], s2["confidence"],
@@ -361,7 +390,7 @@ def three_stage_signal(symbol: str | None = None) -> dict:
361
390
  "as_of": snap_daily.get("as_of"),
362
391
  "stage1_weekly": s1,
363
392
  "stage2_daily": s2,
364
- "stage3_hourly": s3,
393
+ "stage3_4h": s3,
365
394
  "action": action,
366
395
  "reason": reason,
367
396
  "position_hint": position,