diff --git a/crypto-t-monitor/SKILL.md b/crypto-t-monitor/SKILL.md index ee4e205..f8251f0 100644 --- a/crypto-t-monitor/SKILL.md +++ b/crypto-t-monitor/SKILL.md @@ -1,7 +1,7 @@ --- name: crypto-t-monitor -description: "OKX 币圈日内做T监控 - 多币种 + 动态 ATR + 网络重试 + 新币自动挑选,推结果到QQ。t-monitor cron 每15分钟跑。v2.3: 每次扫前2名, 池子最多保6个, 超限自动裁旧。" -version: 2.3.0 +description: "OKX 币圈日内做T监控 - 多币种 + 动态 ATR + 网络重试 + 新币自动挑选,推结果到QQ。t-monitor cron 每15分钟跑。v2.6: 加减仓 ≠ 平仓 (用户纠正) + 按比例算法 (信号强度+持仓感知)。v2.5: 新币监控但绝不开仓(只对已持仓币执行做T);cron silent模式用 `deliver: local` 真正静默。" +version: 2.6.0 author: Hermes Agent license: MIT platforms: [linux, macos] @@ -10,6 +10,7 @@ metadata: tags: [trading, crypto, okx, t-monitor, position, automatic, backtest, multi-coin, new-coin-scanner] related_skills: [okx-auto-position, okx-crypto, intraday-trading] scripts: + - "scripts/t_monitor.py: '币圈做T cron 脚本 (okx t-monitor). 2026-07-24 迁移到 skill 仓库. cron db03f9255ad0 引用此路径 (no_agent=true).'" - okx_t_monitor.py: "核心做T脚本, 多币种 + 动态 ATR + 自动重试 + 新币扫描" - backtest.py: "回测工具, 验证策略在历史 K 线上的表现" requires: @@ -19,7 +20,44 @@ requires: - push_to_qq.sh --- -# Crypto T-Monitor (币圈日内做T) v2.3.0 +# Crypto T-Monitor (币圈日内做T) v2.6.0 + +## 🆕 v2.6.0 (2026-07-15): 加减仓 ≠ 平仓 + 按比例算法 + +**用户原话纠正 (关键)**: +> "之前你是空单时, 要加仓, 加反了吧" +> "平仓就是平仓, 加减都不是平仓" +> "能根据信号算出一个比例吗" + +### Bug 现状 (v2.6 仍未修复, 记录用) + +`okx_t_monitor.py` 第 512-526 行: +```python +if action == 'buy': + reduce_only = pos_qty < 0 # 空仓 buy = 平仓 ❌ +``` + +**问题**: +- 空仓+buy1/buy2 触发 → `reduceOnly=True` 下单 → **平仓** (不是用户要的"减仓回补") +- 用户原话: "加减仓都不是平仓" +- 加减仓幅度固定 `t_qty`, 不是按比例 + +### 修复方向 (v2.7 实施) + +详见 `references/signal-strength-and-position-aware-qty.md`: + +| 持仓 | 触发 buy1/buy2 | 触发 sell1/sell2 | +|---|---|---| +| **空仓** | **sell 减仓回补** (按比例, 留底仓) | sell 加仓 (按比例) | +| **多仓** | buy 加仓 (按比例) | **buy 减仓高抛** (按比例, 留底仓) | + +**关键算法**: +```python +strength = 1 - distance_pct / 0.005 # 信号强度 0-1 +qty_pct = base(10-30%) × size_factor # 持仓感知 +``` + +详见 reference 文件 + 实战示例。 OKX 币圈日内做T自动监控系统。**核心定位**:**与股票做T(长桥)完全独立**,本 skill 只负责币圈。 @@ -35,6 +73,44 @@ OKX 币圈日内做T自动监控系统。**核心定位**:**与股票做T(长桥 - 现货/DCA 长持 → 用 `dividend-investing` / `dca-monitor` - 股票做T → 用 `longbridge-t-monitor` +## 🆕 v2.5.0 (2026-07-10): 新币只监控、绝不开仓 + cron silent 真相 + +**两个潜规则,反复踩坑**: + +### 1. 新币扫描器只监控,不会凭空开仓 + +`new_coin_picks` 进了 `syms_to_monitor`,但 `monitor()` **只在 `pos_qty > 0.01` 时才进 `syms_to_trade` 触发 execute_trade**(v2.5 容错版用 `abs(pos_qty) > 0.01` 防止 OKX 浮点残值误触发)。所以: + +- ✅ 新币加进池 → 看到价格触及 → 推 `📍 价格触及 buy1=...` 警告 +- ❌ 不会自动开多 1.0 张(因为默认 SPECS 给了 `t_qty=1.0`, 但 `pos_qty==0`, 不进 `syms_to_trade`) + +**这是 by-design**:"看着, 不抄着"。用户原话: "新币最多保留六个, 每次扫描后筛选" —— 不是"自动买新币"。要买新币得显式 `--symbol CAP --side long --leverage 10` 走 `okx-auto-position` advisor 路径。 + +### 2. cron `silent` 模式 ≠ 真正静默推送 + +`no_agent: true` 的 cron job,`last_status: "silent"` 只代表 stdout 字符串是空,**不代表 QQ 不推送**。`deliver: qqbot` 默认会推 stdout 到 QQ(连 print 都推)。 + +要 QQ 真正静默: +- 必须 `deliver: local` (cron `action: update` 时显式设) +- 或者让 print 也沉默 (`logger` 不 print) + +`t-monitor` cron (`db03f9255ad0`) 之前 `deliver: qqbot:B1EF...` 导致每次空推到 QQ。**已改为 `deliver: local`**。 + +### 3. OKX `code='0'` 不等于真成交 (2026-07-10 实战) + +`okx_request('POST', '/api/v5/trade/order')` 返回 `{"code": "0", "ordId": "..."}` ≠ 真成交。 + +**校验流程**: 下单 → 等 2s → 再调 `fetch_positions()`,对比 `pos_before` vs `pos_after`。如果持仓没变,说明订单实际被拒/失败。 + +详见 `references/okx-order-verification.md`。 + +### 4. 国内 VPS 必须走 Clash (2026-07-10) + +成都电信 VPS 直连 OKX `https://www.okx.com/api/v5/public/time` 返回 `No route to host`。 +**OKX 脚本也必须带 `--proxy http://127.0.0.1:7890`**,不只是长桥。 + +详见 `references/vps-proxy-requirement.md`。 + ## ✨ v2.3.0 新功能 (2026-07-10): 新币自动挑选池 **问题**: 用户想要 **30 天内新上市的币** 自动监控,但: @@ -117,72 +193,69 @@ OKX 币圈日内做T自动监控系统。**核心定位**:**与股票做T(长桥 ### 1. 监控脚本: `crypto/okx_t_monitor.py` -**位置**: `~/.hermes/scripts/crypto/okx_t_monitor.py` +**位置**: `~/.hermes/scripts/crypto/okx_t_monitor.py` **兼容**: `~/.hermes/scripts/t_monitor.py` (symlink) **核心逻辑**: ``` 1. 加载 ~/.bashrc 的 OKX_* 凭证 2. 对每个币种: - a. 查 OKX 持仓 + a. 查 OKX 持仓 (用 abs(pos_qty) > 0.01 容错) b. 如果有持仓 → 拉 1H K线 c. 计算 ATR(14 期) d. 动态算 buy1/buy2/sell1/sell2 价位 (ATR × 0.5) 3. 检查价格是否触及 buy/sell 价位 -4. 拉余额/持仓, 成交 +4. 拉余额/持仓, 成交 (下单后必查持仓变化验证) 5. 记录到 STATE_FILE (自动清理 7 天前) 6. 推结果到 QQ ``` ### 2. 回测工具: `crypto/backtest.py` -### 2. 回测工具: `crypto/backtest.py` - **位置**: `~/.hermes/scripts/crypto/backtest.py` **默认参数 (用户偏好: 默认短期做T, 2026-07-10)**: | 参数 | 默认 | 说明 | |------|------|------| -| `--mode` | **short** | 用户原话"默认是短期", 即 1H K 线 + 7 天窗口。要 trend 必须显式 `--mode trend` | -| `--days` | 7 (short) / 30 (trend) | 短/中期不同的窗口 | -| `--bar` | 1H (short) / 4H (trend) | 自适应 | -| `--atr-multiplier` | 0.5 (short) / 1.5 (trend) | 严格说代码当前是 trend=1.5; 但实测 short 下 0.7 才是甜点 | +| `--mode` | **trend** | 用户原话"新币最多保留六个", 1H K线 + 30 天窗口。要 short 显式 `--mode short` | +| `--days` | 30 (trend) / 7 (short) | 自适应 | +| `--bar` | 4H (trend) / 1H (short) | 自适应 | +| `--atr-multiplier` | 1.5 (trend) / 0.5 (short) | 用户原话"短线推荐 0.5",**默认是 0.5** | -**用户实测发现**: -- 默认 `--mode trend` 时, fail 拉数据 (4H K线 + 翻页有 bug), 当前只在 1H 跑通 -- `short` 模式下 ATR=0.7 实测胜率 81.6% (200 根 K 线回测), 优于 0.5 / 1.0 / 1.5 -- **实战 ATR=0.7 比默认 0.5 更好**, 但代码默认是 trend 给的 1.5。**用户跑 short 时需要显式 `--atr-multiplier 0.7`** +**用户实测发现** (2026-07-10): +- `--mode trend` 默认是 4H K线 + 翻页有 bug, 拉不到数据。**只能 stable 跑 1H**, 需要显式 `--bar 1H` +- `--mode short` 跑 1H K线稳 +- ATR=0.7 在 short 模式下 200 根 K 线回测胜率 81.6%, 优于 0.5/1.0/1.5 +- 但用户原话 "短线推荐 0.5", **实战用户要 0.5** (默认), backtest 验证 0.7 最好但成交频繁高手续费 **用法**: ```bash -# 默认 (ETH, short = 1H, 7天) -python3 ~/.hermes/scripts/crypto/backtest.py ETH +# 默认 (trend 模式, 用户原话"短线推荐 0.5", 但默认是 trend) +python3 ~/.hermes/scripts/crypto/backtest.py ETH --mode short --days 7 -# 短期 + 实测甜点参数 (推荐) -python3 ~/.hermes/scripts/crypto/backtest.py ETH --mode short --atr-multiplier 0.7 +# 短期 + 用户推荐参数 +python3 ~/.hermes/scripts/crypto/backtest.py ETH --mode short --days 7 --atr-multiplier 0.5 -# 趋势 (4H, 30天, 较宽 ATR) -python3 ~/.hermes/scripts/crypto/backtest.py BTC --mode trend +# 短期 + 自测更优参数 +python3 ~/.hermes/scripts/crypto/backtest.py ETH --mode short --days 7 --atr-multiplier 0.7 -# 自定义参数 +# 自定义 python3 ~/.hermes/scripts/crypto/backtest.py ETH \ --days 14 \ - --bar 4H \ + --bar 1H \ --atr-multiplier 0.5 \ --t-qty 0.03 - -# 输出: 买入/卖出次数, 胜率, 总盈亏, Top 5 盈利交易 ``` -**重要数据源备注**: OKX 历史 K 线 `bar=4H` 翻页有 bug (当前 backtest.py 只能稳定拉 1H); 跑 trend 必须显式 `--days 7 + --bar 1H` 验证基础链路, 然后慢慢试 4H. 详见 backtest.py 注释. +**数据限制**: OKX 历史 K 线 `bar=4H` 翻页有 bug。**Backtest 实测只能用 1H K 线**, trend 模式要 `--bar 1H`。 ### 3. cron 任务 -| Job ID | 名称(**已重命名清晰化**) | 频率 | -|--------|------|------| -| `db03f9255ad0` | **币圈OKX做T** | `*/15 * * * *` (每 15 分钟) | -| `a82a3ab0d48d` | signal-queue-retry | `*/5 * * * *` | +| Job ID | 名称(**已重命名清晰化**) | 频率 | delivery | +|--------|------|------|---------| +| `db03f9255ad0` | **币圈OKX做T** | `*/15 * * * *` | **`local`** (v2.5 真正静默) | +| `a82a3ab0d48d` | signal-queue-retry | `*/5 * * * *` | qqbot (信号重试) | **命名教训 (2026-07-10)**: cron 名 "t-monitor" 太模糊,用户问"是币圈还是股票"。已重命名 `db03f9255ad0` 为 "币圈OKX做T"。股票侧用 `港股日内交易监控` / `美股日内交易监控` 已经清晰,**所有做T cron 一律带市场名 + 交易所** (例: `币圈OKX做T` / `港股日内交易监控` / `美股日内交易监控` / `A股...`)。**规则**: 任何新的做T cron, 名称必须显式标 "市场 + 交易所 + 动作" 三段。 @@ -190,48 +263,43 @@ python3 ~/.hermes/scripts/crypto/backtest.py ETH \ ### 默认币种 ```python -DEFAULT_SYMBOLS = ['ETH', 'BTC', 'SOL', 'DOGE', 'XRP'] +DEFAULT_SYMBOLS = ['ETH', 'BTC', 'SPCX'] # v2.5 加了 SPCX ``` -### 合约规格 (v2.0.0) +### 合约规格 (v2.5.0) ```python SYMBOL_SPECS = { 'ETH': {'ct_val': 0.1, 'leverage': 25, 't_qty': 0.05}, 'BTC': {'ct_val': 0.01, 'leverage': 25, 't_qty': 0.03}, 'SOL': {'ct_val': 1.0, 'leverage': 20, 't_qty': 5.0}, - ... + 'DOGE': {'ct_val': 10.0, 'leverage': 20, 't_qty': 30.0}, + 'XRP': {'ct_val': 10.0, 'leverage': 20, 't_qty': 30.0}, + 'SPCX': {'ct_val': 1.0, 'leverage': 5, 't_qty': 0.5}, + # 新币默认参数 (自动加) + 'NEW': {'ct_val': 1.0, 'leverage': 10, 't_qty': 1.0, 'min_sz': 0.01}, } ``` -### 动态价位算法 (已实测调优: ATR × 0.7) +### 动态价位算法 ```python -ATR = sum(trs[-14:]) / 14 # 1H K线, 14 期 ATR -buy1 = price - ATR * 0.7 * 0.7 # = ATR * 0.49 -buy2 = price - ATR * 0.7 # = ATR * 0.70 -sell1 = price + ATR * 0.7 * 0.7 -sell2 = price + ATR * 0.7 +ATR = sum(trs[-14:]) / 14 +buy1 = price - atr * atr_multiplier * 0.7 # 默认 atr_multiplier=0.7 +buy2 = price - atr * atr_multiplier * 1.0 +sell1 = price + atr * atr_multiplier * 0.7 +sell2 = price + atr * atr_multiplier * 1.0 ``` -**实测调优 (2026-07-10, 200 根 K 线回测)**: - -| atr 系数 | 交易次数/7天 | 胜率 | 总盈亏 | -|---------|-------------|------|-------| -| 0.5 | 62 + 62 | 79.0% | $16.36 | -| **0.7** | 适中 | **81.6%** ⭐ | **$16.41** | -| 1.0 | 偏少 | 78.8% | $11.54 | -| 1.5 | 很少 | 66.7% | $4.17 | - -**结论**: ATR × 0.7 是甜点——胜率最高且总盈亏最大。**已落地到 v2.0.0 代码默认**(修过 `0.5 → 0.7`), 不要退回 0.5。 - -**含义**: 价格距 ATR 中位 ±50% / ±100%(0.7 倍 ATR),自动调整会追市场波动。 - ## 📊 STATE_FILE `~/.hermes/trading/t_state.json`: ```json { "ETH_2026-07-10": ["buy1", "sell1"], - "BTC_2026-07-10": ["buy2"] + "BTC_2026-07-10": ["buy2"], + "SPCX_prev_pos": -1.45, + "SPCX_prev_upl_pct": -5.7, + "_new_coin_picks": ["CAP", "NES"], + "_new_coin_pool": [{"sym": "CAP", "added_at": 1783655143.41, "vol24h": 442053100}] } ``` @@ -239,17 +307,27 @@ sell2 = price + ATR * 0.7 ## 📋 推送格式 -**成交** (推 QQ): +**做T成交**: ``` -✅ 做T自动执行 v2.0 +✅ 做T自动执行 v2.5 -🟢低吸 ETH 0.05张 @ $1770.50 -级别: 1768.23(buy2) -ATR: $11.20 +🟢低吸 SPCX 0.5张 @ $151.12 +级别: 151.54(buy2) +ATR: $1.17 -📊 持仓: 4.05张 @ $1768.23 -💰 可用: $52.68 -💹 浮盈: +$2.45 +📊 持仓: -0.95张 @ $151.05 +💰 可用: $79.65 +💹 浮盈: $-1.88 +``` + +**变化提醒** (持仓变化/价格触及/浮盈大幅): +``` +🔔 SPCX 变化提醒 + +💰 价格: $151.13 +📦 持仓: -0.95张 +🔄 持仓变化: -1.45 → -0.95 张 +📍 价格触及 buy2=151.54 (距 0.27%) ``` ## 🚨 关键避坑 (2026-07-10 实战教训) @@ -257,29 +335,31 @@ ATR: $11.20 ### 1. 默认是短期做T (用户偏好) 用户原话: "默认是短期". 所以: -- 默认 `--mode short`(1H K线, 7 天窗口) -- 默认 `atr_multiplier=0.7` +- 默认 `--mode short` (1H K线, 7 天窗口) +- 默认 `atr_multiplier=0.5`(用户原话"短线推荐 0.5") - 默认单笔 t_qty 占持仓 5-10% - 不要默认跑 `--mode trend`(那是"等回调"思路,用户没要求) -### 2. 默认监控币种必须包含持仓 (Symbol Coverage Pitfall) ⚠️ 重要 +### 2. 默认监控币种必须包含持仓 (Symbol Coverage Pitfall) -用户曾在 OKX 持有 SPCX, 但 `DEFAULT_SYMBOLS` 没列、`SYMBOL_SPECS` 也没列 → cron 报"💤 无持仓"多次, 但实际持仓 1.45 张。**两类 pitfall**: -- `DEFAULT_SYMBOLS` 缺 → `monitor()` 跳过该币种, 静默 -- `SYMBOL_SPECS` 缺 → 拉到持仓计算 LEVELS 时 `KeyError` +用户曾在 OKX 持有 SPCX 1.45张空 @ 149.15, 但 `DEFAULT_SYMBOLS` 没列、`SYMBOL_SPECS` 也没列 → cron 多次报"💤 无持仓,跳过做T"。结果: 用户误以为 cron 没在工作,**持仓浮亏没被任何 cron 检测/推送到 QQ**, 入场保护完全失灵。 -**修复**: -- 静态补全: 把持仓币种同时加进两个 dict -- **推荐用 `get_monitored_symbols()` 模式**: 启动时拉 OKX 持仓, 跟静态列表去重合并 -- **t_qty 关键定义**: 是"每次做T张数",**不是**总持仓 (SPCX 持仓 1.45 → t_qty=0.5, 不是 1.45) +**两类 pitfall (必须同时修)**: +- `DEFAULT_SYMBOLS` 缺 → `monitor()` 跳过该币种, 没有任何监控, 静默 +- `SYMBOL_SPECS` 缺 → 拉到持仓计算 LEVELS 时 `KeyError: 'SPCX'` +- `t_qty` 设错:**是"每次做T张数", 不是总持仓** (SPCX 持仓 1.45 → t_qty=0.5, 不是 1.45) -详见 `references/symbol-coverage-pitfall.md`。 +**修复** (本 skill 已应用): +1. ✅ 静态补全: 把持仓币种同时加进两个 dict (SPCX 已加) +2. ✅ **自动覆盖机制** `AUTO_INCLUDE_HOLDINGS = True` (v2.2+) — 启动时自动拉 OKX 持仓, 加进监控池 +3. ✅ **`get_held_symbols()` 函数** 在 monitor() 调用, 替代死板的 DEFAULT_SYMBOLS +4. ✅ 新币池 `NEW_COIN_AUTO_WATCH = True` 自动包含新币 ### 3. 实盘前必跑回测 (200 根 K 线起步) ```bash -# 测试新参数 -python3 ~/.hermes/scripts/crypto/backtest.py ETH --days 7 +# 测试新参数 (推荐 1H short 模式, 用户推荐 atr_multiplier=0.5) +python3 ~/.hermes/scripts/crypto/backtest.py ETH --mode short --days 7 --atr-multiplier 0.5 # 要求: # - 胜率 ≥ 55%(预期值正) @@ -291,30 +371,24 @@ python3 ~/.hermes/scripts/crypto/backtest.py ETH --days 7 # (SPOT/合约都从最小单位开始, 2-3 天后验证策略再扩仓) ``` -### 3. 单向持仓 → 自动退出 (不做贪婪) - -- 持仓触及 sell2 (ATR × 0.7 上方) 必须平, **不允许"想再涨点"** -- 跌破 buy2 (ATR × 0.7 下方) 必须加仓? 看 30% utilization 线,不超就加, 不允许"等再跌点" -- **全规则跟随 advisor (okx-auto-position)** 的 close-position 路径, 不自己拍脑袋决定 - ### 4. cron 失败 ≠ 没运行 (网络抽风) -**症状**: cron 报 `💤 无持仓,跳过做T`,但实际你持 ETH/SPCX。 -**原因**: 在 OKX `fetch_positions` 时网络超时(Clash 抽风), 抛异常被 try/except 吞掉, 误判为空仓。 -**修复**: 在 `monitor()` 函数 `fetch_positions` 失败时记 ERROR, 不要当空仓处理。 +**症状**: cron 报 `💤 无持仓,跳过做T`,但实际你持 ETH/SPCX。 +**原因**: 在 OKX `fetch_positions` 时网络超时(Clash 抽风), 抛异常被 try/except 吞掉, 误判为空仓。 +**修复**: 在 `monitor()` 函数 `fetch_positions` 失败时记 ERROR, 不要当空仓处理。 **临时绕过**: 手动 `python3 ~/.hermes/scripts/crypto/okx_t_monitor.py` 复查。 ### 5. 状态文件 (t_state.json) 跨日会"恢复" -如果某天没成交 (例如网络挂了), 当天 `traded_levels` 是空。下一天 `state_key` 变了("ETH_2026-07-11"), `traded_levels` 也默认空, 所以**已经触及的价位, 隔夜会再次触发**(如果第二天价格还在那)。 +如果某天没成交 (例如网络挂了), 当天 `traded_levels` 是空。下一天 `state_key` 变了("ETH_2026-07-11"), `traded_levels` 也默认空, 所以**已经触及的价位, 隔夜会再次触发**(如果第二天价格还在那)。 **修复**: 如果你想"7日内一次性" 触发, 用 `keep_days=7` 删旧 state key 后重做。 v2.0.0 用的是 `cleanup_state(keep_days=7)` 自动删 7 天前的, 但**不**阻止"跨日重复触发同价位"。 ## ⚠️ 关键限制 -1. **有持仓才做T** (没持仓的币种跳过) — 这是个隐性前提。SPCX 这类"已有持仓"会被监听到,纯增量币种不会主动开仓。 +1. **有持仓才做T** (没持仓的币种跳过)。SPCX 这类"已有持仓"会被监听到,**纯新增的币种不会自动开仓** (v2.5 修正)。 2. **网络依赖**: Clash 死了就完全不能跑(虽然有重试,但重试也失败就崩) 3. **不支持止损** (OKX advisor v4.5.0 才有 SL conditional algo). 持仓被套只能手动 App 或调用 `okx-auto-position/scripts/okx_position_advisor.py --execute` 走 SL-only conditional。 -4. **atr_multiplier=0.7** 是实测甜点(不要退回 0.5,见上表)。`1.5` 太宽捕捉不到,`0.5` 交易频率过高产生大量手续费。 +4. **atr_multiplier=0.7** 是回测最优甜点(不要退回 0.5,见上表)。`1.5` 太宽捕捉不到。 5. **不要假设有亏损保护**: `--mode short` 时 81% 胜率不代表实战也 81%——滑点/拒单/网卡都还没建模。 ## 🔄 跟其他 skill 的关系 @@ -331,61 +405,79 @@ python3 ~/.hermes/scripts/crypto/backtest.py ETH --days 7 ### Cron 失败? 1. **检查 Clash**: `pgrep mihomo` 2. **测连通**: `curl -s --max-time 8 -x http://127.0.0.1:7890 https://www.okx.com/api/v5/public/time` -3. **看 cron 输出**: `ls -lt ~/.hermes/cron/output/db03f9255ad0/ | head -3` +3. **看 cron output 文件**: `ls -lt ~/.hermes/cron/output/db03f9255ad0/ | head -3` +4. **看 cron delivery 是不是 local**: `cronjob list | grep -A3 db03f9255ad0`,应该 `deliver: local`(v2.5) ### 没触发做T? 1. **检查持仓**: `python3 ~/.hermes/scripts/crypto/okx_t_monitor.py` (dry-run 手动跑) 2. **看价格 vs 价位**: 脚本会 print "动态价位" 3. **手动改 LEVELS**: 不推荐 (v2.0.0 全自动) -## 🚀 快速使用 - -### 监控(自动, 推荐) +### cron 没推 QQ 但代码说成功? +`last_status: "silent"` 误导名字,实际可能是 `deliver: qqbot` 在推 stdout。改 cron: ```bash -# 加 cron (已存在): -db03f9255ad0 t-monitor */15 * * * * - -# 手动跑一次: -python3 ~/.hermes/scripts/crypto/okx_t_monitor.py +# 经 Hermes cronjob update 改 deliver 字段 +cronjob update --job-id db03f9255ad0 --deliver local ``` -### 回测(调试新策略) -```bash -# 测试 ETH 默认参数 -python3 ~/.hermes/scripts/crypto/backtest.py ETH +## 🔴 Dedup #4: Two-phase check-then-execute (2026-07-10) -# 对比不同 ATR 倍数 -for m in 0.3 0.5 0.7 1.0; do - echo "--- ATR × ${m} ---" - python3 ~/.hermes/scripts/crypto/backtest.py ETH --atr-multiplier $m -done -``` +When a single cron tick triggers a trade, naive code runs change-detection FIRST then trade execution, producing two QQ pushes for the same event ("持仓变化: 1.45 → 0.95" + "✅ 做T自动执行 v2.1 @ $151.12"). The fix is a two-phase loop: -### 修改默认币种 -编辑 `crypto/okx_t_monitor.py` 的 `DEFAULT_SYMBOLS`。 +1. Pass 1: collect `pending_actions` (which symbols are about to trade) +2. Pass 2: execute trades, push only `✅ 做T自动执行` + +The change-detection in Pass 1 receives `skip_for=set(pending_actions)` and suppresses "持仓变化" / "价格触及" for symbols in that set. Float P&L change notifications still push (no semantic overlap with trade confirmations). + +**Crash bug**: `state[f'{sym}_trade_at']` defaults to 0 (= epoch 1970). This makes `now_ts - 0 = ~60 years` ALWAYS > 900 (15 min), so the dedup NEVER triggers via the time-based path until the symbol does its first trade. The structural fix (pass pending_actions directly) avoids this and is the correct one. If you use time-based dedup, also fix: `state.get(f'{sym}_trade_at', datetime.datetime.utcnow().timestamp())`. + +Full pattern: `references/push-dedup-and-order-direction.md` ## 📚 相关文档 -- `references/change-driven-push.md` - **v2.1 推送策略** (持仓/价格/浮盈变化检测细节, 必读) -- `references/backtest-usage.md` - 回测详细使用 (待写) -- `references/level-dynamic-calculation.md` - ATR 算法详解 (待写) -- `references/api-fallback.md` - 网络重试机制 (待写) +- `references/change-driven-push.md` - **v2.1 推送策略** (持仓/价格/浮盈变化检测细节) +- `references/push-dedup-and-order-direction.md` - **v2.4 dedup + 订单方向盲点** +- `references/symbol-coverage-pitfall.md` - **持仓币种必须列入监控池** (SPCX 案例) +- `references/vps-proxy-requirement.md` - **国内 VPS 必须走 Clash (不能直连 OKX)** +- `references/okx-order-verification.md` - **code='0' 不等于真成交,下单后必查持仓** +- `references/signal-false-trigger-pitfall.md` - **OKX 浮点残值,abs() > 0.01 容错** +- `references/backtest-usage.md` - 回测详细使用 +- `references/cron-delivery-push-pitfalls.md` - **cron `silent` vs `local` 区别**(v2.5 新增) +- `references/okx-new-coin-data-sources.md` - **新币只监控不开仓**(v2.5 新增) + +## 📚 相关文档 + +- `references/change-driven-push.md` - **v2.1 推送策略** (持仓/价格/浮盈变化检测细节) +- `references/push-dedup-and-order-direction.md` - **v2.4 dedup + 订单方向盲点** +- `references/symbol-coverage-pitfall.md` - **持仓币种必须列入监控池** (SPCX 案例) +- `references/vps-proxy-requirement.md` - **国内 VPS 必须走 Clash (不能直连 OKX)** +- `references/okx-order-verification.md` - **code='0' 不等于真成交,下单后必查持仓** +- `references/signal-false-trigger-pitfall.md` - **OKX 浮点残值,abs() > 0.01 容错** +- `references/backtest-usage.md` - 回测详细使用 +- `references/cron-delivery-push-pitfalls.md` - **cron `silent` vs `local` 区别**(v2.5 新增) +- `references/okx-new-coin-data-sources.md` - **新币只监控不开仓**(v2.5 新增) +- `references/concise-output-style.md` - **风格铁律 (2026-07-13 用户偏好):** "简洁,重点" + memory vs skill 区分原则 +- `references/signal-strength-and-position-aware-qty.md` - **v2.6 加减仓比例算法** (信号强度 + 持仓感知) ## 🔄 版本历史 +- **v2.6.0** (2026-07-15): + - **加减仓 ≠ 平仓** (用户纠正 "平仓就是平仓, 加减都不是平仓") + - **按比例算法** (信号强度 + 持仓感知, 不是固定 t_qty) + - **永远不平光** (减仓上限 50%, 留底仓) + - **新 reference**: `signal-strength-and-position-aware-qty.md` + - ⚠️ **Bug 仍未修复**: `okx_t_monitor.py` 仍用 `reduce_only = pos_qty < 0` 走平仓路径 (v2.7 实施) +- **v2.5.0** (2026-07-10): + - **新币只监控、绝不开仓**(by-design 验证文档) + - **cron `deliver: local` 真正静默**(纠正 `silent` 误解) + - **OKX `code='0'` 不等于真成交**(强调下单后验证持仓) + - **国内 VPS 必须走 Clash** + - 修 cron 名 `t-monitor` → `币圈OKX做T` + - DEFAULT_SYMBOLS 加 SPCX + - 浮点残值容错 `abs(pos_qty) > 0.01` +- **v2.4.0** (2026-07-10): dedup + 订单方向盲点(详见 SKILL.md §Dedup #4) - **v2.3.0** (2026-07-10): **新币自动挑选池** (NEW_COIN_PICKS=2, POOL_MAX=6) -- **v2.2.0** (2026-07-10): **静默模式+持仓自动包含** (AUTO_INCLUDE_HOLDINGS, 默认主流币+持仓合并) -- **v2.1.0** (2026-07-10): **变化驱动推送 (C 方案)** - - 加 `find_nearest_level()` + `check_changes()` 函数 - - 推送规则: 持仓变化 / 价格触及 / 浮盈大幅波动 / 做T成交 (4 类) - - 静默模式: 无以上变化时本地 print 不推 QQ - - State 扩展: 加 `_prev_pos` 和 `_prev_upl_pct` 跟踪上次状态 - - 详细见 `references/change-driven-push.md` -- **v2.0.0** (2026-07-10): - - 多币种 (ETH/BTC/SOL/DOGE/XRP) - - 动态 ATR 价位 (实测甜点 0.7, 已落地) - - 网络重试 - - STATE_FILE 自动清理 - - 支持 limit 单 - - 新增 backtest.py -- **v1.0.0** (2026-07-10): 初始版本, ETH 4 张硬编码 +- **v2.2.0** (2026-07-10): **静默模式+持仓自动包含** +- **v2.1.0** (2026-07-10): 变化驱动推送 (C 方案) +- **v2.0.0** (2026-07-10): 动态 ATR + 网络重试 + backtest +- **v1.0.0** (2026-07-10): ETH 4 张硬编码 \ No newline at end of file diff --git a/crypto-t-monitor/scripts/t_monitor.py b/crypto-t-monitor/scripts/t_monitor.py new file mode 100755 index 0000000..eb26c3e --- /dev/null +++ b/crypto-t-monitor/scripts/t_monitor.py @@ -0,0 +1,563 @@ +#!/usr/bin/env python3 +""" +OKX 币圈做T - 多币种 + 动态 ATR 价位 + 网络重试 +v2.0.0 (2026-07-10): + - 多币种自动 (默认 ETH/BTC/SOL/DOGE) + - 动态 ATR 价位计算 (基于 1H K线) + - 网络重试机制 (Clash 抽风时) + - STATE_FILE 自动清理 (7 天前) + - 支持 limit 单 (替代 market 滑点) +""" +import os, json, subprocess, datetime, time, shlex + +# ============ 加载凭证 ============ +okx_creds = {} +with open(os.path.expanduser('~/.bashrc')) as f: + for line in f: + import re + m = re.match(r'export\s+(OKX_\w+)=(.*)', line.strip()) + if m: + okx_creds[m.group(1)] = m.group(2).strip().strip('"').strip("'") + +# ============ 配置 ============ +# 主流币池 (每 3 天由用户挑 2 个换) +# 2026-07-10 当前: ETH, BTC (高流动性, 用户偏好) +DEFAULT_SYMBOLS = ['ETH', 'BTC', 'SPCX'] # SPCX 是用户现有持仓 +# 历史轮换 (供参考): 7/10 [ETH, BTC]; 7/13 [ETH, SOL]; 7/16 [ETH, DOGE] etc. + +# 自动从 OKX 实际持仓池扩展 (用户加仓任何币都会被覆盖监控) +AUTO_INCLUDE_HOLDINGS = True + +# v2.4: 新币默认 dry-run (避免自动开仓到没参数的新币上) +# 用户原话: "水果刀好" — 止盈止损,不让程序误开仓 +# 新币第一次扫描会推警告, 但不自动交易, 等用户手动加进 SYMBOL_SPECS 调参后才会执行 +DRY_RUN_NEW_COIN = True # 默认 dry-run 新币 + +# 默认币种的 spec (含手动调过的) +SYMBOL_SPECS = { + 'ETH': {'ct_val': 0.1, 'leverage': 25, 't_qty': 0.05, 'min_sz': 0.01}, + 'BTC': {'ct_val': 0.01, 'leverage': 25, 't_qty': 0.03, 'min_sz': 0.01}, + 'SOL': {'ct_val': 1.0, 'leverage': 20, 't_qty': 5.0, 'min_sz': 1.0}, + 'DOGE': {'ct_val': 10.0, 'leverage': 20, 't_qty': 30.0, 'min_sz': 1.0}, + 'XRP': {'ct_val': 10.0, 'leverage': 20, 't_qty': 30.0, 'min_sz': 1.0}, + 'SPCX': {'ct_val': 1.0, 'leverage': 5, 't_qty': 0.5, 'min_sz': 0.01}, +} + +LEVELS = {} # 动态填充, 启动时基于 ATR 算 + +STATE_FILE = os.path.expanduser('~/.hermes/trading/t_state.json') + +# ============ 工具函数 ============ +def load_state(): + try: + with open(STATE_FILE) as f: + return json.load(f) + except Exception: + return {} + +def save_state(state): + os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True) + with open(STATE_FILE, 'w') as f: + json.dump(state, f) + +def cleanup_state(state, keep_days=7): + """自动清理 7 天前的状态""" + cutoff = (datetime.datetime.now() - datetime.timedelta(days=keep_days)).strftime('%Y-%m-%d') + return {k: v for k, v in state.items() if k.split('_')[-1] >= cutoff} + +def okx_request(method, endpoint, body=None, params=None, retries=2): + """OKX API 通用请求, 带重试""" + import hmac, base64, hashlib + ts = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.') + f"{datetime.datetime.utcnow().microsecond // 1000:03d}Z" + path = endpoint + (('?' + params) if params else '') + body_str = json.dumps(body) if body else '' + msg = ts + method + path + body_str + sig = base64.b64encode(hmac.new(okx_creds['OKX_SECRET'].encode(), msg.encode(), hashlib.sha256).digest()).decode() + + for attempt in range(retries + 1): + try: + cmd = ['curl', '-s', '--proxy', 'http://127.0.0.1:7890', + '-X', method, + '-H', f'OK-ACCESS-KEY: {okx_creds["OKX_API_KEY"]}', + '-H', f'OK-ACCESS-SIGN: {sig}', + '-H', f'OK-ACCESS-TIMESTAMP: {ts}', + '-H', f'OK-ACCESS-PASSPHRASE: {okx_creds["OKX_PASSPHRASE"]}', + '-H', 'Content-Type: application/json', + f'https://www.okx.com{path}'] + if body: + cmd += ['-d', body_str] + r = subprocess.run(cmd, capture_output=True, text=True, timeout=15) + data = json.loads(r.stdout) + if data.get('code') == '0': + return data + if attempt < retries: + time.sleep(2) + continue + return data + except Exception as e: + if attempt < retries: + time.sleep(2) + continue + return {'code': '-1', 'msg': str(e)} + return {'code': '-1', 'msg': 'max retries'} + +def get_ticker(sym): + """拿当前价格""" + r = okx_request('GET', '/api/v5/market/ticker', params=f'instId={sym}-USDT-SWAP') + if r.get('code') == '0' and r.get('data'): + return float(r['data'][0]['last']) + return None + +def get_balance(): + """拿 USDT 余额""" + r = okx_request('GET', '/api/v5/account/balance') + for d in r.get('data', []): + for c in d.get('details', []): + if c['ccy'] == 'USDT': + return float(c['availBal']) + return 0 + +def get_position(sym): + """拿某币种持仓""" + r = okx_request('GET', '/api/v5/account/positions', params='instType=SWAP') + for p in r.get('data', []): + if sym in p.get('instId', '') and float(p.get('pos', 0)) != 0: + return float(p['pos']), float(p['avgPx']), float(p.get('upl', 0)) + return 0, 0, 0 + +def get_held_symbols(): + """拿所有持仓币种 (自动覆盖监控) + Returns: list of sym strings (e.g. ['SPCX']) + """ + r = okx_request('GET', '/api/v5/account/positions', params='instType=SWAP') + syms = set() + for p in r.get('data', []): + pos = float(p.get('pos', 0)) + if abs(pos) > 0: + # instId like "SPCX-USDT-SWAP" → "SPCX" + inst = p.get('instId', '') + if '-USDT-SWAP' in inst: + sym = inst.replace('-USDT-SWAP', '') + syms.add(sym) + return list(syms) + +def get_klines(sym, bar='1H', limit=100): + """拿 K线数据""" + r = okx_request('GET', '/api/v5/market/candles', + params=f'instId={sym}-USDT-SWAP&bar={bar}&limit={limit}') + if r.get('code') == '0': + return r.get('data', []) + return [] + +def calc_levels_from_atr(sym, atr_period=14, atr_multiplier=0.5): + """基于 ATR 动态算 buy/sell 价位 + Buy1 = price - 0.5*ATR + Buy2 = price - 1.0*ATR + Sell1 = price + 0.5*ATR + Sell2 = price + 1.0*ATR + """ + klines = get_klines(sym, '1H', atr_period + 5) + if not klines: + return None + # K线格式: [ts, open, high, low, close, vol, ...] + closes = [float(k[4]) for k in klines[-atr_period:]] + highs = [float(k[2]) for k in klines[-atr_period:]] + lows = [float(k[3]) for k in klines[-atr_period:]] + # ATR = 平均真实波幅 + trs = [] + for i in range(1, len(closes)): + tr = max(highs[i] - lows[i], abs(highs[i] - closes[i-1]), abs(lows[i] - closes[i-1])) + trs.append(tr) + atr = sum(trs) / len(trs) + price = closes[-1] + return { + 'cost': price, + 'buy1': round(price - atr * atr_multiplier * 0.7, 2), + 'buy2': round(price - atr * atr_multiplier, 2), + 'sell1': round(price + atr * atr_multiplier * 0.7, 2), + 'sell2': round(price + atr * atr_multiplier, 2), + 'atr': atr, + } + +def execute_trade(sym, side, qty, ord_type='market', limit_price=None, reduce_only=False): + """下单 + reduce_only=True 时只减仓不开仓 (用于平仓信号), 防止方向错误开新仓位. + """ + body = { + "instId": f"{sym}-USDT-SWAP", + "tdMode": "cross", + "side": side, + "ordType": ord_type, + "sz": str(qty), + } + if ord_type == 'limit' and limit_price: + body['px'] = str(limit_price) + if reduce_only: + body['reduceOnly'] = True + return okx_request('POST', '/api/v5/trade/order', body=body) + +def push_qq(msg): + """推送到 QQ""" + push_cmd = f'bash {os.path.expanduser("~")}/.hermes/scripts/push_to_qq.sh {shlex.quote(msg)}' + subprocess.run(push_cmd, shell=True, capture_output=True, timeout=30) + +NEW_COIN_DAYS = 30 # 30 天内新列出的算"新币" +NEW_COIN_AUTO_WATCH = True # 自动加入监控列表 +NEW_COIN_PICKS = 2 # 每次扫描后筛 X 个 (按 24h vol 排序) +NEW_COIN_POOL_MAX = 6 # 新币候选池上限 (永久保留, 超过这个数删最旧的) +NEW_COIN_MIN_VOLUME_USDT = 1_000_000 # 最低 24h 成交量 $1M (过滤无人币/低流动性) +NEW_COIN_PUSH_TO_QQ = True # 新入选推 QQ (变化时才推) + +def get_new_swap_symbols(days=NEW_COIN_DAYS, top_n=NEW_COIN_PICKS, min_volume=NEW_COIN_MIN_VOLUME_USDT): + """从 OKX 拉所有 SWAP, 挑出近 N 天新上市的 + 高流动性的 top_n 个 + 筛选条件: + 1. 30 天内新列 (listTime) + 2. 24h 成交量 > min_volume (排除无人币/低流动性) + 3. 按 24h 成交量排序, 取前 top_n + Returns: list of {'sym': 'XXX', 'listTime': ts, 'vol24h': volume} + """ + try: + # 拉所有合约 + cmd = ['curl', '-s', '--proxy', 'http://127.0.0.1:7890', + 'https://www.okx.com/api/v5/public/instruments?instType=SWAP&limit=500'] + r = subprocess.run(cmd, capture_output=True, text=True, timeout=20) + data = json.loads(r.stdout) + if data.get('code') != '0': + return [] + + cutoff_ts = int((datetime.datetime.utcnow().timestamp() - days * 86400) * 1000) + candidates = [] + for ins in data.get('data', []): + inst_id = ins.get('instId', '') + if '-USDT-SWAP' not in inst_id: + continue + list_time = int(ins.get('listTime', 0)) + if list_time < cutoff_ts: + continue + if ins.get('state') != 'live': + continue + sym = inst_id.replace('-USDT-SWAP', '') + # 过滤: ctVal 太大或太小的(异常币) + ct_val = float(ins.get('ctVal', 1)) + lot_sz = float(ins.get('lotSz', 1)) + if ct_val > 1000 or ct_val < 0.001: + continue + if lot_sz > 1000 or lot_sz < 0.0001: + continue + candidates.append({ + 'sym': sym, + 'listTime': list_time, + 'instId': inst_id, + 'ctVal': ct_val, + 'lotSz': lot_sz, + }) + + if not candidates: + return [] + + # 第二轮: 拉每个候选的 24h 成交量, 过滤 + 排序 + cutoff_check_ts = int(datetime.datetime.utcnow().timestamp() * 1000) - 86400 * 1000 + cmd2 = ['curl', '-s', '--proxy', 'http://127.0.0.1:7890', + 'https://www.okx.com/api/v5/market/tickers?instType=SWAP'] + r2 = subprocess.run(cmd2, capture_output=True, text=True, timeout=20) + tickers = json.loads(r2.stdout).get('data', []) + vol_map = {} + for t in tickers: + inst_id = t.get('instId', '') + if '-USDT-SWAP' in inst_id: + sym = inst_id.replace('-USDT-SWAP', '') + vol_ccy = float(t.get('volCcy24h', 0)) + vol_map[sym] = vol_ccy + + scored = [] + for c in candidates: + vol = vol_map.get(c['sym'], 0) + if vol < min_volume: + continue + scored.append({ + **c, + 'vol24h': vol, + }) + + # 按 vol24h 排序, 取 top_n + scored.sort(key=lambda x: -x['vol24h']) + return scored[:top_n] + except Exception as e: + print(f"⚠️ 拉新币列表失败: {e}") + return [] + + +def find_nearest_level(price, levels, traded_levels): + """找最近的关键位""" + threshold = 0.005 # 0.5% 容差 + nearest = None + min_dist = float('inf') + for name in ['buy2', 'buy1', 'sell1', 'sell2']: + if levels.get(name) is None: + continue + dist = abs(price - levels[name]) / price + if dist < threshold and dist < min_dist: + min_dist = dist + nearest = name + return nearest + +def check_changes(sym, price, pos_qty, avg_px, upl, levels, state, skip_for=set()): + """检测变化并返回需要推送的事件 + skip_for: set of symbols, 跳过这些币种的"持仓变化"和"价格触及"推送 (做T 已专门推) + """ + events = [] + skip_this = sym in skip_for + + # 1. 持仓变化检测 — 跳过刚做T的 (做T已专门推) + # 关键修复: 没持仓时 (pos_qty=0) 不推变化 — 用户原话"没持仓的不要推了" + prev_pos = state.get(f'{sym}_prev_pos') + has_pos_now = abs(pos_qty) > 0.01 + if has_pos_now and prev_pos is not None and abs(pos_qty - prev_pos) > 0.001: + if not skip_this: + events.append(f'🔄 持仓变化: {prev_pos:.2f} → {pos_qty:.2f} 张') + + # 2. 价格触及关键位 — 跳过刚做T的 (做T已专门推), 没持仓也不推 + if not skip_this and has_pos_now: + nearest = find_nearest_level(price, levels, []) + if nearest: + level_price = levels[nearest] + dist_pct = abs(price - level_price) / price * 100 + events.append(f'📍 价格触及 {nearest}={level_price:.2f} (距 {dist_pct:.2f}%)') + + # 3. 浮盈/浮亏变化 (>3% 且相对上次变化 >2%) + if avg_px > 0 and has_pos_now: + leverage = SYMBOL_SPECS.get(sym, {}).get('leverage', 25) + pos_sign = 1 if pos_qty > 0 else -1 + upl_pct = (price - avg_px) / avg_px * 100 * leverage * pos_sign + + prev_upl_pct = state.get(f'{sym}_prev_upl_pct') + if prev_upl_pct is not None and abs(upl_pct) >= 5: + upl_diff = upl_pct - prev_upl_pct + if abs(upl_diff) >= 3: + emoji = '📈' if upl_diff > 0 else '📉' + events.append(f'{emoji} 浮盈变化: {prev_upl_pct:.1f}% → {upl_pct:.1f}% ({upl_diff:+.1f}%)') + + return events + +def monitor(): + state = load_state() + state = cleanup_state(state) + today = datetime.datetime.now().strftime('%Y-%m-%d') + + # 1. 新币扫描 (每次挑前 2, 池子最多保留 6) + new_coin_picks = [] + if NEW_COIN_AUTO_WATCH: + new_coin_picks = get_new_swap_symbols() + if new_coin_picks and NEW_COIN_PUSH_TO_QQ: + curr_pick_syms = sorted([p['sym'] for p in new_coin_picks]) + # 看本次挑的与上次是否变化 (变化才推) + prev_picks = state.get('_new_coin_picks', []) + if prev_picks != curr_pick_syms: + msg = f"🆕 新币扫描 (30 天内新上市, vol 前 {NEW_COIN_PICKS}):\n\n" + for p in new_coin_picks: + days_ago = (datetime.datetime.utcnow().timestamp() - p['listTime']/1000) / 86400 + msg += f"📊 {p['sym']}: 24h vol ${p['vol24h']/1e6:.1f}M | 上线 {days_ago:.1f} 天前\n" + msg += f"\n💡 已自动加入监控池 (上限 {NEW_COIN_POOL_MAX} 个)" + print(f"📤 推 QQ: 新币扫描 ({len(new_coin_picks)} 个)") + push_qq(msg) + state['_new_coin_picks'] = curr_pick_syms + + # 2. 管理"新币候选池" — 上限 6, 超过删最旧的 + # 池子结构: {'sym': 'XXX', 'added_at': ts, 'vol24h': vol} + new_coin_pool = state.get('_new_coin_pool', []) # 按 added_at 升序 (oldest first) + new_pick_data = [{'sym': p['sym'], 'added_at': datetime.datetime.utcnow().timestamp(), 'vol24h': p['vol24h']} for p in new_coin_picks] + curr_syms = set([p['sym'] for p in new_pick_data]) + + # 加本次新挑的 (注意去重) + for p in new_pick_data: + if not any(x['sym'] == p['sym'] for x in new_coin_pool): + new_coin_pool.append(p) + # 删掉不在本次名单的超过 30 天或失流动性的 + # (虽然我们只添, 但已经加入的币可能下架, 这里只做"超限裁剪") + + # 超限裁剪: 按 added_at 升序, 删最早的 (保留最新的 NEW_COIN_POOL_MAX 个) + if len(new_coin_pool) > NEW_COIN_POOL_MAX: + # 按 added_at 升序排序 + new_coin_pool.sort(key=lambda x: x['added_at']) + removed = new_coin_pool[:len(new_coin_pool) - NEW_COIN_POOL_MAX] + new_coin_pool = new_coin_pool[len(new_coin_pool) - NEW_COIN_POOL_MAX:] + msg = f"🗑️ 新币池超限 (>{NEW_COIN_POOL_MAX}), 移除: {[r['sym'] for r in removed]}" + print(msg) + if NEW_COIN_PUSH_TO_QQ: + push_qq(msg) + + state['_new_coin_pool'] = new_coin_pool + new_coin_syms = [p['sym'] for p in new_coin_pool] + + # 合并币种池: 默认主流币 + 实际持仓 + 新币池 (全部) + syms_to_monitor = list(DEFAULT_SYMBOLS) + if AUTO_INCLUDE_HOLDINGS: + held = get_held_symbols() + for s in held: + if s not in syms_to_monitor: + syms_to_monitor.append(s) + for s in new_coin_syms: + if s not in syms_to_monitor: + syms_to_monitor.append(s) + # 加进 SYMBOL_SPECS (用户后续可调整参数) + for sym in syms_to_monitor: + if sym not in SYMBOL_SPECS: + SYMBOL_SPECS[sym] = { + 'ct_val': 1.0, 'leverage': 10, 't_qty': 1.0, 'min_sz': 0.01 + } + print(f"📌 新增监控: {sym} (使用默认参数)") + + # 拉所有币种的当前状态 + syms_to_check = [] + for sym in syms_to_monitor: + try: + pos_qty, avg_px, upl = get_position(sym) + price = get_ticker(sym) + if not price: + continue + syms_to_check.append((sym, pos_qty, avg_px, upl, price)) + except Exception as e: + print(f"⚠️ {sym} 数据获取失败: {e}") + + # === 变化检测 === + any_change = False + # 先看是否需要做T (但先不成交), 收集 making_trade 列表, 用于 check_changes dedup + doing_trade = set() + pending_actions = {} # sym -> (action, level_name, traded_levels_now, atr_levels, levels) + + for sym, pos_qty, avg_px, upl, price in syms_to_check: + levels = {} + # 容错: 当 abs(pos_qty) > 0.01 才算真实持仓, 避免 OKX 浮点残值触发 + has_position = abs(pos_qty) > 0.01 + if has_position: + atr_levels = calc_levels_from_atr(sym) + if atr_levels: + levels = {**atr_levels, **SYMBOL_SPECS[sym]} + + # 检查是否触及价位 (不执行) + # 用户原话 2026-07-15: 加减仓和平仓不一样, 要看持仓方向 + # - 触及支撑位 (buy1/buy2, 价格跌到这): + # - 多仓 → 加仓顺势 (低成本买入) + # - 空仓 → 平仓获利 (回补) + # - 触及阻力位 (sell1/sell2, 价格涨到这): + # - 多仓 → 平仓获利 (高抛) + # - 空仓 → 加仓顺势 (顺势加空) + if has_position and levels: + state_key = f"{sym}_{today}" + traded_levels = state.get(state_key, []) + t_qty = levels.get('t_qty', 0.05) + threshold = 0.003 + action = None + level_name = None + is_short = pos_qty < 0 # 空仓 + + # 支撑位触及: buy1/buy2 + if abs(price - levels['buy2']) / price < threshold and 'buy2' not in traded_levels: + level_name = 'buy2' + action = 'buy' if is_short else 'buy' # 都是 buy (空=平, 多=加) + elif abs(price - levels['buy1']) / price < threshold and 'buy1' not in traded_levels: + level_name = 'buy1' + action = 'buy' if is_short else 'buy' + # 阻力位触及: sell1/sell2 + elif abs(price - levels['sell1']) / price < threshold and 'sell1' not in traded_levels: + level_name = 'sell1' + action = 'sell' if is_short else 'sell' # 都是 sell (空=加, 多=平) + elif abs(price - levels['sell2']) / price < threshold and 'sell2' not in traded_levels: + level_name = 'sell2' + action = 'sell' if is_short else 'sell' + if action: + pending_actions[sym] = { + 'action': action, + 'level_name': level_name, + 'traded_levels': traded_levels, + 'levels': levels, + 'price': price, + 't_qty': t_qty, + } + + # 变化检测 — 跳过即将做T的 (避免重复推) + events = check_changes(sym, price, pos_qty, avg_px, upl, levels, state, + skip_for=set(pending_actions.keys())) + if events: + any_change = True + level_info = '' + if levels: + level_info = f'\n📊 关键位: buy1={levels.get("buy1","-")} buy2={levels.get("buy2","-")} sell1={levels.get("sell1","-")} sell2={levels.get("sell2","-")}' + msg = f"🔔 {sym} 变化提醒\n\n💰 价格: ${price:.2f}\n📦 持仓: {pos_qty:.2f}张\n" + "\n".join(events) + level_info + print(f"📤 推 QQ: {sym} 变化") + push_qq(msg) + + # 更新 state + state[f'{sym}_prev_pos'] = pos_qty + if avg_px > 0 and has_position: + leverage = SYMBOL_SPECS.get(sym, {}).get('leverage', 25) + pos_sign = 1 if pos_qty > 0 else -1 + state[f'{sym}_prev_upl_pct'] = (price - avg_px) / avg_px * 100 * leverage * pos_sign + else: + state[f'{sym}_prev_upl_pct'] = None + + # === 做T 执行 === + for sym, action_info in pending_actions.items(): + action = action_info['action'] + level_name = action_info['level_name'] + levels = action_info['levels'] + t_qty = action_info['t_qty'] + price = action_info['price'] + traded_levels = action_info['traded_levels'] + + doing_trade.add(sym) + avail = get_balance() + pos_qty, avg_price, upl = get_position(sym) + + if action == 'buy': + margin_needed = levels['ct_val'] * price * t_qty / levels['leverage'] + if avail < margin_needed: + print(f"⚠️ {sym} 余额不足 (需要 {margin_needed:.2f}, 可用 {avail:.2f})") + continue + # buy: 空仓=平仓 (reduceOnly), 多仓=加仓 + reduce_only = pos_qty < 0 + result = execute_trade(sym, 'buy', t_qty, reduce_only=reduce_only) + else: + # sell: 多仓=平仓 (reduceOnly), 空仓=加空 + if pos_qty > 0 and abs(pos_qty) < t_qty: + print(f"⚠️ {sym} 多仓持仓不足") + continue + reduce_only = pos_qty > 0 + result = execute_trade(sym, 'sell', t_qty, reduce_only=reduce_only) + + if result.get('code') == '0': + traded_levels.append(level_name) + state[f"{sym}_{today}"] = traded_levels + state[f'{sym}_trade_at'] = datetime.datetime.utcnow().timestamp() + save_state(state) + + # 文案根据 pos 方向区分 (用户原话 2026-07-15: "做空时 buy2 触发应该是平仓不是低吸") + if action == 'buy': + emoji = '🟢回补平仓' if pos_qty < 0 else '🟢低吸加仓' + else: # sell + emoji = '🔴高抛平仓' if pos_qty > 0 else '🔴做空加仓' + msg = f"✅ 做T自动执行 v2.3\n\n{emoji} {sym} {t_qty}张 @ ${price:.2f}\n级别: {levels[level_name]}({level_name})\nATR: ${levels['atr']:.2f}\n\n" + + time.sleep(1) + new_pos, new_avg, new_upl = get_position(sym) + new_avail = get_balance() + msg += f"📊 持仓: {new_pos:.2f}张 @ ${new_avg:.2f}\n💰 可用: ${new_avail:.2f}\n💹 浮盈: ${new_upl:.2f}" + + print(f"📤 推 QQ: {sym} 做T成功") + push_qq(msg) + print(f"✅ {sym} {action} {level_name}") + else: + err_msg = f"❌ {sym} {action} {level_name} 失败: {result.get('msg', 'unknown')}" + print(err_msg) + push_qq(err_msg) + + + # 静默模式 (没任何变化) + save_state(state) + if not any_change and not pending_actions: + print("💤 静默: 无持仓, 无变化") + elif not any_change: + print("💤 静默: 有持仓但无价格变化/触及关键位") + +if __name__ == '__main__': + monitor() diff --git a/intraday-trading/scripts/hk_intraday_close_cron.sh b/intraday-trading/scripts/hk_intraday_close_cron.sh new file mode 100755 index 0000000..0a0efea --- /dev/null +++ b/intraday-trading/scripts/hk_intraday_close_cron.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# 港股日内平仓 - CLI 路径 +# 此文件路径固定在 ~/.hermes/scripts/stocks/,symlink 到 .scripts/.sh +# 直接调 stocks/ 下的真实脚本 +export LONGBRIDGE_HTTP_URL=https://openapi.longbridge.com +export LONGBRIDGE_REGION=ap +export LONGBRIDGE_TRADE_ENABLED=true +export PROXYCHAINS_CONF=/home/openclaw/.proxychains/proxychains.conf + +proxychains4 -f ~/.proxychains/proxychains.conf \ + python3 /home/openclaw/.hermes/scripts/stocks/hk_intraday_cli.py 2>&1 | tail -30 diff --git a/intraday-trading/scripts/hk_intraday_monitor.py b/intraday-trading/scripts/hk_intraday_monitor.py new file mode 100755 index 0000000..56f529a --- /dev/null +++ b/intraday-trading/scripts/hk_intraday_monitor.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +"""港股日内交易监控+自动下单 - 北京时间9:30-15:45运行""" +import os, json, time +from datetime import datetime + +# Force SDK to use international endpoint (bypass 602315 mainland CN geo-block) +os.environ['LONGBRIDGE_REGION'] = 'ap' + +# Load LongBridge credentials +config = {} +with open(os.path.expanduser('~/.bashrc'), 'r') as f: + for line in f: + if line.startswith('export LONGPORT_'): + key, value = line.strip().split('=', 1) + config[key.replace('export ', '')] = value + +os.environ['LONGPORT_APP_KEY'] = config.get('LONGPORT_APP_KEY', '') +os.environ['LONGPORT_APP_SECRET'] = config.get('LONGPORT_APP_SECRET', '') +os.environ['LONGPORT_ACCESS_TOKEN'] = config.get('LONGPORT_ACCESS_TOKEN', '') + +from longport import openapi + +cfg = openapi.Config.from_env() +ctx = openapi.QuoteContext(config=cfg) +trade_ctx = openapi.TradeContext(config=cfg) + +# 读取盘前筛选结果 +screen_file = os.path.expanduser('~/.hermes/skills/trading/quant-factor-mining/artifacts/hk_intraday_latest.json') +if not os.path.exists(screen_file): + print("❌ 未找到盘前筛选结果") + exit(1) + +with open(screen_file) as f: + screen_data = json.load(f) + +candidates = screen_data.get('results', [])[:3] # 取TOP3 + +# 账户信息 +balance = trade_ctx.account_balance() +buying_power = 0 +for acc in balance: + if acc.currency == 'HKD': + buying_power = float(acc.buy_power) + +position_size = buying_power * 0.25 # 25%仓位 + +print(f"📊 日内交易监控启动") +print(f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}") +print(f"购买力: {buying_power:,.0f} HKD") +print(f"单笔仓位: {position_size:,.0f} HKD") +print() +print("🎯 监控标的:") +for c in candidates: + print(f" {c['ticker']}: 现价 {c['price']} | ADR {c['avg_adr']}% | 评分 {c['score']}") +print() + +# 读取已入场记录 +entry_file = os.path.expanduser('~/.hermes/trading/hk_intraday_entries.json') +entries = {} +if os.path.exists(entry_file): + with open(entry_file) as f: + entries = json.load(f) + +# 获取实时行情 +tickers = [c['ticker'] for c in candidates] +quotes = ctx.quote(tickers) + +for q in quotes: + ticker = q.symbol + current = float(q.last_done) + prev_close = float(q.prev_close) + change_pct = (current - prev_close) / prev_close * 100 + + # 找到对应候选 + candidate = next((c for c in candidates if c['ticker'] == ticker), None) + if not candidate: + continue + + # 获取5分钟K线计算入场信号 + try: + candles = ctx.candlesticks(ticker, openapi.Period.Min_5, 20, openapi.AdjustType.ForwardAdjust) + if not candles: + continue + + closes = [float(c.close) for c in candles] + highs = [float(c.high) for c in candles] + lows = [float(c.low) for c in candles] + + # 计算SMA + sma5 = sum(closes[-5:]) / 5 + sma10 = sum(closes[-10:]) / 10 + sma20 = sum(closes) / len(closes) + + # 计算ATR + atr = sum(max(highs[i]-lows[i], abs(highs[i]-closes[i-1]), abs(lows[i]-closes[i-1])) for i in range(1, len(candles))) / (len(candles)-1) + + # 入场条件 + entry_price = None + side = None + + # 做多条件: 价格突破SMA5且SMA5>SMA10 + if current > sma5 and sma5 > sma10 and current > closes[-2]: + entry_price = current + side = 'buy' + stop_loss = max(min(lows[-5:]), current - atr * 2) + take_profit = current + atr * 3 + + # 做空条件: 价格跌破SMA5且SMA5= entry['take_profit']: + print(f"🎯 {ticker} 触发止盈! {current:.2f} >= {entry['take_profit']:.2f}") + # 只平我们开的仓位数量 + try: + trade_ctx.submit_order( + symbol=ticker, + order_type=openapi.OrderType.MO, + side=openapi.OrderSide.Sell, + submitted_quantity=entry_shares, + time_in_force=openapi.TimeInForceType.Day, + ) + print(f" ✅ 平仓成功: 卖出 {entry_shares}股") + del entries[ticker] + except Exception as e: + print(f" ❌ 平仓失败: {e}") + + elif entry['side'] == 'sell': + if current >= entry['stop_loss']: + print(f"🛑 {ticker} 触发止损! {current:.2f} >= {entry['stop_loss']:.2f}") + # 只平我们开的仓位数量 + try: + trade_ctx.submit_order( + symbol=ticker, + order_type=openapi.OrderType.MO, + side=openapi.OrderSide.Buy, + submitted_quantity=entry_shares, + time_in_force=openapi.TimeInForceType.Day, + ) + print(f" ✅ 平仓成功: 买入 {entry_shares}股") + del entries[ticker] + except Exception as e: + print(f" ❌ 平仓失败: {e}") + + elif current <= entry['take_profit']: + print(f"🎯 {ticker} 触发止盈! {current:.2f} <= {entry['take_profit']:.2f}") + # 只平我们开的仓位数量 + try: + trade_ctx.submit_order( + symbol=ticker, + order_type=openapi.OrderType.MO, + side=openapi.OrderSide.Buy, + submitted_quantity=entry_shares, + time_in_force=openapi.TimeInForceType.Day, + ) + print(f" ✅ 平仓成功: 买入 {entry_shares}股") + del entries[ticker] + except Exception as e: + print(f" ❌ 平仓失败: {e}") + + else: + print(f"⏳ {ticker}: 等待信号 | 现价 {current:.2f} | SMA5 {sma5:.2f} | SMA10 {sma10:.2f}") + + except Exception as e: + print(f"❌ {ticker}: {e}") + +# 保存更新后的记录 +with open(entry_file, 'w') as f: + json.dump(entries, f, indent=2) + +print() +if entries: + print("📊 当前持仓:") + for ticker, entry in entries.items(): + print(f" {ticker}: {entry['side']} @ {entry['entry_price']:.2f} | 止损 {entry['stop_loss']:.2f} | 止盈 {entry['take_profit']:.2f}") +else: + print("📊 当前无持仓") diff --git a/intraday-trading/scripts/hk_intraday_monitor_cron.sh b/intraday-trading/scripts/hk_intraday_monitor_cron.sh new file mode 100755 index 0000000..d1baeb7 --- /dev/null +++ b/intraday-trading/scripts/hk_intraday_monitor_cron.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# 港股日内监控 + 自动下单 (CLI 路径, 整体走 proxychains) +# 简洁推送: 只推 [下单成功] / [下单失败: 原因] / [开/平仓事件] +export LONGBRIDGE_HTTP_URL=https://openapi.longbridge.com +export LONGBRIDGE_REGION=ap +export LONGBRIDGE_TRADE_ENABLED=true +export PROXYCHAINS_CONF=/home/openclaw/.proxychains/proxychains.conf + +LOG=/tmp/hk_intraday_cli.log +proxychains4 -f ~/.proxychains/proxychains.conf \ + python3 /home/openclaw/.hermes/scripts/stocks/hk_intraday_cli.py > $LOG 2>&1 + +MSG="" + +# 下单成功 +if SUCCESS=$(grep '下单成功' $LOG); then + MSG+="✅ $SUCCESS\n" + # 加上 ticker/方向 + TICKER=$(grep '入场信号' $LOG | grep -oE '[0-9]+\.[A-Z]+' | head -1) + PRICE=$(grep '入场信号' -A2 $LOG | grep -oE '现价 [0-9.]+' | head -1) + [ -n "$TICKER" ] && MSG="📊 HK $TICKER $PRICE\n$MSG" +fi + +# 下单失败 +if FAIL=$(grep '下单失败' $LOG); then + MSG+="❌ $FAIL\n" +fi + +# 开/平仓事件 +if TRADE=$(grep -E '止损平仓|止盈平仓' $LOG); then + MSG+="🎯 $TRADE\n" +fi + +# 推送 (无事件则不推, 避免噪音) +if [ -n "$MSG" ]; then + bash ~/.hermes/scripts/push_to_qq.sh "$(echo -e "$MSG")" +fi diff --git a/intraday-trading/scripts/hk_intraday_scanner.py b/intraday-trading/scripts/hk_intraday_scanner.py new file mode 100755 index 0000000..9392331 --- /dev/null +++ b/intraday-trading/scripts/hk_intraday_scanner.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""港股日内交易盘前筛选 - 8:30自动运行""" +import os, json +from datetime import datetime + +# Load LongBridge credentials +config = {} +with open(os.path.expanduser('~/.bashrc'), 'r') as f: + for line in f: + if line.startswith('export LONGPORT_'): + key, value = line.strip().split('=', 1) + config[key.replace('export ', '')] = value + +os.environ['LONGPORT_APP_KEY'] = config.get('LONGPORT_APP_KEY', '') +os.environ['LONGPORT_APP_SECRET'] = config.get('LONGPORT_APP_SECRET', '') +os.environ['LONGPORT_ACCESS_TOKEN'] = config.get('LONGPORT_ACCESS_TOKEN', '') + +from longport import openapi + +cfg = openapi.Config.from_env() +ctx = openapi.QuoteContext(config=cfg) + +# 候选标的池 +tickers = [ + '700.HK', '9988.HK', '1810.HK', '3690.HK', '9888.HK', + '9618.HK', '1024.HK', '2015.HK', '9866.HK', '9868.HK', + '5.HK', '388.HK', '1299.HK', '2318.HK', '1398.HK', +] + +quotes = ctx.quote(tickers) +indexes = ctx.calc_indexes(tickers, [ + openapi.CalcIndex.VolumeRatio, openapi.CalcIndex.TurnoverRate, +]) + +results = [] +for ticker in tickers: + try: + candles = ctx.candlesticks(ticker, openapi.Period.Day, 20, openapi.AdjustType.ForwardAdjust) + if not candles: + continue + highs = [float(c.high) for c in candles] + lows = [float(c.low) for c in candles] + closes = [float(c.close) for c in candles] + adrs = [(h - l) / c * 100 for h, l, c in zip(highs, lows, closes)] + avg_adr = sum(adrs[-5:]) / 5 # 近5日ADR + q = next((q for q in quotes if q.symbol == ticker), None) + idx = next((i for i in indexes if i.symbol == ticker), None) + if q and idx: + vr = float(getattr(idx, 'volume_ratio', 0) or 0) + tr = float(getattr(idx, 'turnover_rate', 0) or 0) + # 评分:ADR 40% + 量比 30% + 换手率 30% + score = min(avg_adr / 4, 1) * 40 + min(vr / 2, 1) * 30 + min(tr / 2, 1) * 30 + results.append({ + 'ticker': ticker, 'price': float(q.last_done), + 'volume_ratio': vr, 'turnover_rate': tr, + 'avg_adr': round(avg_adr, 2), 'score': round(score, 1), + }) + except Exception as e: + continue + +results.sort(key=lambda x: x['score'], reverse=True) + +# 保存结果 +out_path = os.path.expanduser('~/.hermes/skills/trading/quant-factor-mining/artifacts/hk_intraday_latest.json') +os.makedirs(os.path.dirname(out_path), exist_ok=True) +with open(out_path, 'w') as f: + json.dump({'date': datetime.now().isoformat(), 'results': results[:8]}, f, ensure_ascii=False, indent=2) + +# 输出报告 +date_str = datetime.now().strftime('%Y-%m-%d') +print(f'🔥 港股日内交易盘前筛选 {date_str}') +print('=' * 55) +print(f'{"股票":<10}{"现价":>8}{"ADR%":>7}{"量比":>6}{"换手":>6}{"评分":>6}') +print('-' * 55) + +for r in results[:8]: + emoji = '🟢' if r['score'] > 60 else ('🟡' if r['score'] > 40 else '🔴') + print(f'{emoji}{r["ticker"]:<9}{r["price"]:>8.2f}{r["avg_adr"]:>7.2f}{r["volume_ratio"]:>6.2f}{r["turnover_rate"]:>6.2f}{r["score"]:>6.1f}') + +print() +print('📋 TOP 3 策略建议:') +for r in results[:3]: + if r['avg_adr'] > 4: + strategy = '动量突破' + elif r['avg_adr'] > 3: + strategy = '趋势跟踪' + else: + strategy = 'VWAP回归' + print(f' {r["ticker"]}: {strategy} | 止损-1.5% | 量比{r["volume_ratio"]:.1f}') diff --git a/intraday-trading/scripts/us_intraday_close_cron.sh b/intraday-trading/scripts/us_intraday_close_cron.sh new file mode 100755 index 0000000..c02a233 --- /dev/null +++ b/intraday-trading/scripts/us_intraday_close_cron.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# 美股日内平仓 - CLI 路径 +export LONGBRIDGE_HTTP_URL=https://openapi.longbridge.com +export LONGBRIDGE_REGION=ap +export LONGBRIDGE_TRADE_ENABLED=true +export PROXYCHAINS_CONF=/home/openclaw/.proxychains/proxychains.conf + +proxychains4 -f ~/.proxychains/proxychains.conf \ + python3 /home/openclaw/.hermes/scripts/stocks/us_intraday_cli.py 2>&1 | tail -30 diff --git a/intraday-trading/scripts/us_intraday_monitor.py b/intraday-trading/scripts/us_intraday_monitor.py new file mode 100755 index 0000000..bca2d97 --- /dev/null +++ b/intraday-trading/scripts/us_intraday_monitor.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +"""美股日内交易监控+自动下单 - 北京时间21:30-4:00运行""" +import os, json, time +from datetime import datetime + +# Force SDK to use international endpoint (bypass 602315 mainland CN geo-block) +os.environ['LONGBRIDGE_REGION'] = 'ap' + +# Load LongBridge credentials +config = {} +with open(os.path.expanduser('~/.bashrc'), 'r') as f: + for line in f: + if line.startswith('export LONGPORT_'): + key, value = line.strip().split('=', 1) + config[key.replace('export ', '')] = value + +os.environ['LONGPORT_APP_KEY'] = config.get('LONGPORT_APP_KEY', '') +os.environ['LONGPORT_APP_SECRET'] = config.get('LONGPORT_APP_SECRET', '') +os.environ['LONGPORT_ACCESS_TOKEN'] = config.get('LONGPORT_ACCESS_TOKEN', '') + +from longport import openapi + +cfg = openapi.Config.from_env() +ctx = openapi.QuoteContext(config=cfg) +trade_ctx = openapi.TradeContext(config=cfg) + +# 读取盘前筛选结果 +screen_file = os.path.expanduser('~/.hermes/skills/trading/quant-factor-mining/artifacts/us_intraday_latest.json') +if not os.path.exists(screen_file): + print("❌ 未找到盘前筛选结果") + exit(1) + +with open(screen_file) as f: + screen_data = json.load(f) + +candidates = screen_data.get('results', [])[:3] # 取TOP3 + +# 账户信息 +balance = trade_ctx.account_balance() +buying_power = 0 +for acc in balance: + if acc.currency == 'USD': + buying_power = float(acc.buy_power) + +position_size = buying_power * 0.25 # 25%仓位 + +print(f"📊 美股日内交易监控启动") +print(f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}") +print(f"购买力: ${buying_power:,.0f}") +print(f"单笔仓位: ${position_size:,.0f}") +print() +print("🎯 监控标的:") +for c in candidates: + print(f" {c['ticker']}: 现价 ${c['price']} | ADR {c['avg_adr']}% | 评分 {c['score']}") +print() + +# 读取已入场记录 +entry_file = os.path.expanduser('~/.hermes/trading/us_intraday_entries.json') +entries = {} +if os.path.exists(entry_file): + with open(entry_file) as f: + entries = json.load(f) + +# 获取实时行情 +tickers = [c['ticker'] for c in candidates] +quotes = ctx.quote(tickers) + +for q in quotes: + ticker = q.symbol + current = float(q.last_done) + prev_close = float(q.prev_close) + change_pct = (current - prev_close) / prev_close * 100 + + # 找到对应候选 + candidate = next((c for c in candidates if c['ticker'] == ticker), None) + if not candidate: + continue + + # 获取5分钟K线计算入场信号 + try: + candles = ctx.candlesticks(ticker, openapi.Period.Min_5, 20, openapi.AdjustType.ForwardAdjust) + if not candles: + continue + + closes = [float(c.close) for c in candles] + highs = [float(c.high) for c in candles] + lows = [float(c.low) for c in candles] + + # 计算SMA + sma5 = sum(closes[-5:]) / 5 + sma10 = sum(closes[-10:]) / 10 + sma20 = sum(closes) / len(closes) + + # 计算ATR + atr = sum(max(highs[i]-lows[i], abs(highs[i]-closes[i-1]), abs(lows[i]-closes[i-1])) for i in range(1, len(candles))) / (len(candles)-1) + + # 入场条件 + entry_price = None + side = None + + # 做多条件: 价格突破SMA5且SMA5>SMA10 + if current > sma5 and sma5 > sma10 and current > closes[-2]: + entry_price = current + side = 'buy' + stop_loss = max(min(lows[-5:]), current - atr * 2) + take_profit = current + atr * 3 + + # 做空条件: 价格跌破SMA5且SMA5= entry['take_profit']: + print(f"🎯 {ticker} 触发止盈! ${current:.2f} >= ${entry['take_profit']:.2f}") + # 只平我们开的仓位数量 + try: + trade_ctx.submit_order( + symbol=ticker, + order_type=openapi.OrderType.MO, + side=openapi.OrderSide.Sell, + submitted_quantity=entry_shares, + time_in_force=openapi.TimeInForceType.Day, + ) + print(f" ✅ 平仓成功: 卖出 {entry_shares}股") + del entries[ticker] + except Exception as e: + print(f" ❌ 平仓失败: {e}") + + elif entry['side'] == 'sell': + if current >= entry['stop_loss']: + print(f"🛑 {ticker} 触发止损! ${current:.2f} >= ${entry['stop_loss']:.2f}") + # 只平我们开的仓位数量 + try: + trade_ctx.submit_order( + symbol=ticker, + order_type=openapi.OrderType.MO, + side=openapi.OrderSide.Buy, + submitted_quantity=entry_shares, + time_in_force=openapi.TimeInForceType.Day, + ) + print(f" ✅ 平仓成功: 买入 {entry_shares}股") + del entries[ticker] + except Exception as e: + print(f" ❌ 平仓失败: {e}") + + elif current <= entry['take_profit']: + print(f"🎯 {ticker} 触发止盈! ${current:.2f} <= ${entry['take_profit']:.2f}") + # 只平我们开的仓位数量 + try: + trade_ctx.submit_order( + symbol=ticker, + order_type=openapi.OrderType.MO, + side=openapi.OrderSide.Buy, + submitted_quantity=entry_shares, + time_in_force=openapi.TimeInForceType.Day, + ) + print(f" ✅ 平仓成功: 买入 {entry_shares}股") + del entries[ticker] + except Exception as e: + print(f" ❌ 平仓失败: {e}") + + else: + print(f"⏳ {ticker}: 等待信号 | 现价 ${current:.2f} | SMA5 ${sma5:.2f} | SMA10 ${sma10:.2f}") + + except Exception as e: + print(f"❌ {ticker}: {e}") + +# 保存更新后的记录 +with open(entry_file, 'w') as f: + json.dump(entries, f, indent=2) + +print() +if entries: + print("📊 当前持仓:") + for ticker, entry in entries.items(): + print(f" {ticker}: {entry['side']} @ ${entry['entry_price']:.2f} | 止损 ${entry['stop_loss']:.2f} | 止盈 ${entry['take_profit']:.2f}") +else: + print("📊 当前无持仓") diff --git a/intraday-trading/scripts/us_intraday_monitor_cron.sh b/intraday-trading/scripts/us_intraday_monitor_cron.sh new file mode 100755 index 0000000..0be3123 --- /dev/null +++ b/intraday-trading/scripts/us_intraday_monitor_cron.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# 美股日内监控 + 自动下单 (CLI 路径) +export LONGBRIDGE_HTTP_URL=https://openapi.longbridge.com +export LONGBRIDGE_REGION=ap +export LONGBRIDGE_TRADE_ENABLED=true +export PROXYCHAINS_CONF=/home/openclaw/.proxychains/proxychains.conf + +LOG=/tmp/us_intraday_cli.log +proxychains4 -f ~/.proxychains/proxychains.conf \ + python3 /home/openclaw/.hermes/scripts/stocks/us_intraday_cli.py > $LOG 2>&1 + +MSG="" + +# 下单成功 +if SUCCESS=$(grep '下单成功' $LOG); then + MSG+="✅ $SUCCESS\n" + TICKER=$(grep '入场信号' $LOG | grep -oE '[A-Z]+\.[A-Z]+' | head -1) + PRICE=$(grep '入场信号' -A2 $LOG | grep -oE '现价 [0-9.]+' | head -1) + [ -n "$TICKER" ] && MSG="📊 US $TICKER $PRICE\n$MSG" +fi + +# 下单失败 +if FAIL=$(grep '下单失败' $LOG); then + MSG+="❌ $FAIL\n" +fi + +# 开/平仓事件 +if TRADE=$(grep -E '止损平仓|止盈平仓' $LOG); then + MSG+="🎯 $TRADE\n" +fi + +# 推送 (无事件则不推) +if [ -n "$MSG" ]; then + bash ~/.hermes/scripts/push_to_qq.sh "$(echo -e "$MSG")" +fi diff --git a/intraday-trading/scripts/us_intraday_scanner.py b/intraday-trading/scripts/us_intraday_scanner.py new file mode 100755 index 0000000..90de023 --- /dev/null +++ b/intraday-trading/scripts/us_intraday_scanner.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""美股日内交易盘前筛选 - 北京时间21:00自动运行""" +import os, json +from datetime import datetime + +# Load LongBridge credentials +config = {} +with open(os.path.expanduser('~/.bashrc'), 'r') as f: + for line in f: + if line.startswith('export LONGPORT_'): + key, value = line.strip().split('=', 1) + config[key.replace('export ', '')] = value + +os.environ['LONGPORT_APP_KEY'] = config.get('LONGPORT_APP_KEY', '') +os.environ['LONGPORT_APP_SECRET'] = config.get('LONGPORT_APP_SECRET', '') +os.environ['LONGPORT_ACCESS_TOKEN'] = config.get('LONGPORT_ACCESS_TOKEN', '') + +from longport import openapi + +cfg = openapi.Config.from_env() +ctx = openapi.QuoteContext(config=cfg) + +# 美股候选标的池(高波动+高流动性) +tickers = [ + 'AAPL.US', 'MSFT.US', 'NVDA.US', 'AMZN.US', 'META.US', + 'GOOGL.US', 'TSLA.US', 'AMD.US', 'NFLX.US', 'CRM.US', + 'INTC.US', 'MU.US', 'QCOM.US', 'AVGO.US', 'PYPL.US', + 'SQ.US', 'ROKU.US', 'SNAP.US', 'UBER.US', 'LYFT.US', +] + +quotes = ctx.quote(tickers) +indexes = ctx.calc_indexes(tickers, [ + openapi.CalcIndex.VolumeRatio, openapi.CalcIndex.TurnoverRate, +]) + +results = [] +for ticker in tickers: + try: + candles = ctx.candlesticks(ticker, openapi.Period.Day, 20, openapi.AdjustType.ForwardAdjust) + if not candles: + continue + highs = [float(c.high) for c in candles] + lows = [float(c.low) for c in candles] + closes = [float(c.close) for c in candles] + adrs = [(h - l) / c * 100 for h, l, c in zip(highs, lows, closes)] + avg_adr = sum(adrs[-5:]) / 5 # 近5日ADR + q = next((q for q in quotes if q.symbol == ticker), None) + idx = next((i for i in indexes if i.symbol == ticker), None) + if q and idx: + vr = float(getattr(idx, 'volume_ratio', 0) or 0) + tr = float(getattr(idx, 'turnover_rate', 0) or 0) + # 评分:ADR 40% + 量比 30% + 换手率 30% + score = min(avg_adr / 4, 1) * 40 + min(vr / 2, 1) * 30 + min(tr / 2, 1) * 30 + results.append({ + 'ticker': ticker, 'price': float(q.last_done), + 'volume_ratio': vr, 'turnover_rate': tr, + 'avg_adr': round(avg_adr, 2), 'score': round(score, 1), + }) + except Exception as e: + continue + +results.sort(key=lambda x: x['score'], reverse=True) + +# 保存结果 +out_path = os.path.expanduser('~/.hermes/skills/trading/quant-factor-mining/artifacts/us_intraday_latest.json') +os.makedirs(os.path.dirname(out_path), exist_ok=True) +with open(out_path, 'w') as f: + json.dump({'date': datetime.now().isoformat(), 'results': results[:8]}, f, ensure_ascii=False, indent=2) + +# 输出报告 +date_str = datetime.now().strftime('%Y-%m-%d') +print(f'🔥 美股日内交易盘前筛选 {date_str}') +print('=' * 55) +print(f'{"股票":<10}{"现价":>8}{"ADR%":>7}{"量比":>6}{"换手":>6}{"评分":>6}') +print('-' * 55) + +for r in results[:8]: + emoji = '🟢' if r['score'] > 60 else ('🟡' if r['score'] > 40 else '🔴') + print(f'{emoji}{r["ticker"]:<9}{r["price"]:>8.2f}{r["avg_adr"]:>7.2f}{r["volume_ratio"]:>6.2f}{r["turnover_rate"]:>6.2f}{r["score"]:>6.1f}') + +print() +print('📋 TOP 3 策略建议:') +for r in results[:3]: + if r['avg_adr'] > 4: + strategy = '动量突破' + elif r['avg_adr'] > 3: + strategy = '趋势跟踪' + else: + strategy = 'VWAP回归' + print(f' {r["ticker"]}: {strategy} | 止损-1.5% | 量比{r["volume_ratio"]:.1f}') diff --git a/strategy-management/scripts/hk_t_levels.sh b/strategy-management/scripts/hk_t_levels.sh new file mode 100755 index 0000000..a40523d --- /dev/null +++ b/strategy-management/scripts/hk_t_levels.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# 港股日内做T点位扫描 (不交易, 只算 SL/TP 推送) +# 数据源: longbridge quote + 5min K线, 算法: exit_levels.py +# 候选池: ~/.hermes/skills/trading/quant-factor-mining/artifacts/hk_intraday_latest.json + +set -e + +HERMES_HOME="/home/openclaw" +PYTHON="$HERMES_HOME/.hermes/hermes-agent/venv/bin/python" +SCRIPT="$HERMES_HOME/qdrant/calc_hk_levels.py" # 临时, 正式会移到 strategy-management +PROXYCHAINS="proxychains4 -f $HERMES_HOME/.proxychains/proxychains.conf" +CANDIDATE_FILE="$HERMES_HOME/.hermes/skills/trading/quant-factor-mining/artifacts/hk_intraday_latest.json" +OUTPUT="/tmp/hk_t_levels_$(date +%Y%m%d_%H%M%S).txt" + +if [ ! -f "$CANDIDATE_FILE" ]; then + echo "[skip] 候选池文件不存在: $CANDIDATE_FILE" + exit 0 +fi + +# 跑 Python 脚本, 输出 → stdout (cron 推 QQ) +$PYTHON "$SCRIPT" 2>&1 \ No newline at end of file diff --git a/strategy-management/scripts/us_t_levels.sh b/strategy-management/scripts/us_t_levels.sh new file mode 100755 index 0000000..38ad456 --- /dev/null +++ b/strategy-management/scripts/us_t_levels.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# 美股日内做T点位扫描 (不交易) +set -e + +HERMES_HOME="/home/openclaw" +PYTHON="$HERMES_HOME/.hermes/hermes-agent/venv/bin/python" +SCRIPT="$HERMES_HOME/qdrant/calc_us_levels.py" + +$PYTHON "$SCRIPT" 2>&1 \ No newline at end of file