feat: strategy-management 加 --strategy 参数 + 新建 A 股 calc_cn_levels.py
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
"""---
|
||||
name: cn-scan
|
||||
description: "A股日内做T点位扫描 — 高股息候选池 + 实时 quote + 日线/5min K, 算 SL/TP1/TP2 (不交易, 仅参考)"
|
||||
---"""
|
||||
|
||||
"""
|
||||
A股日内做T点位扫描 (cron 模板)
|
||||
- 高股息候选池 (预设, 与 scan_cn.py 共用)
|
||||
- 拉实时 quote + 日线/5min K 线 (longport_http)
|
||||
- 算 SL/TP1/TP2 用 calc_exit_levels()
|
||||
- 三级输出: ✅ R:R≥1.5 / ⚠️ R:R 1.0 / ❌ 否决
|
||||
- 推 QQ (origin delivery)
|
||||
|
||||
用法:
|
||||
python3 calc_cn_levels.py # 跑 (默认)
|
||||
python3 calc_cn_levels.py --top 3 # 只看 top 3
|
||||
python3 calc_cn_levels.py --period day # 用日线 (默认)
|
||||
python3 calc_cn_levels.py --strategy turtle_breakout # 海龟
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, '/home/openclaw/.hermes/scripts')
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from longport_http import get_quote, get_candlesticks
|
||||
from exit_levels import calc_exit_levels
|
||||
from indicators import atr as calc_atr, vwap as calc_vwap
|
||||
|
||||
# === A 股候选池 (与 scan_cn.py 共用) ===
|
||||
A_SHARE_POOL = {
|
||||
"601088.SH": {"name": "中国神华", "yield": 6.7, "sector": "煤炭"},
|
||||
"601328.SH": {"name": "交通银行", "yield": 6.2, "sector": "银行"},
|
||||
"601398.SH": {"name": "工商银行", "yield": 5.9, "sector": "银行"},
|
||||
"601288.SH": {"name": "农业银行", "yield": 5.8, "sector": "银行"},
|
||||
"601939.SH": {"name": "建设银行", "yield": 6.0, "sector": "银行"},
|
||||
"601988.SH": {"name": "中国银行", "yield": 5.7, "sector": "银行"},
|
||||
"600900.SH": {"name": "长江电力", "yield": 3.8, "sector": "电力"},
|
||||
"601857.SH": {"name": "中国石油", "yield": 5.5, "sector": "能源"},
|
||||
"600028.SH": {"name": "中国石化", "yield": 5.2, "sector": "能源"},
|
||||
"601728.SH": {"name": "中国电信", "yield": 4.8, "sector": "电信"},
|
||||
"600036.SH": {"name": "招商银行", "yield": 4.5, "sector": "银行"},
|
||||
"601166.SH": {"name": "兴业银行", "yield": 5.8, "sector": "银行"},
|
||||
"601818.SH": {"name": "光大银行", "yield": 5.9, "sector": "银行"},
|
||||
"600377.SH": {"name": "宁沪高速", "yield": 6.2, "sector": "高速"},
|
||||
"601666.SH": {"name": "平煤股份", "yield": 6.2, "sector": "煤炭"},
|
||||
"600023.SH": {"name": "浙能电力", "yield": 5.5, "sector": "电力"},
|
||||
"000858.SZ": {"name": "五粮液", "yield": 10.5, "sector": "白酒"},
|
||||
"000568.SZ": {"name": "泸州老窖", "yield": 7.0, "sector": "白酒"},
|
||||
"000937.SZ": {"name": "冀中能源", "yield": 11.0, "sector": "煤炭"},
|
||||
"002304.SZ": {"name": "洋河股份", "yield": 10.8, "sector": "白酒"},
|
||||
"000596.SZ": {"name": "古井贡酒", "yield": 6.9, "sector": "白酒"},
|
||||
"000001.SZ": {"name": "平安银行", "yield": 5.4, "sector": "银行"},
|
||||
"600519.SH": {"name": "贵州茅台", "yield": 5.0, "sector": "白酒"},
|
||||
}
|
||||
|
||||
MIN_YIELD = 5.0 # 股息率下限
|
||||
|
||||
|
||||
def calc_levels(symbol: str, klines: list, quote: dict, side: str, min_rr: float,
|
||||
strategy: str = 'rsi2_revert'):
|
||||
"""
|
||||
strategy: 'rsi2_revert' | 'vwap_revert' | 'early_bird' | 'turtle_breakout'
|
||||
A 股默认用 rsi2_revert (震荡回归, 适合高股息股)
|
||||
"""
|
||||
if not klines or not quote:
|
||||
return None, '数据缺失'
|
||||
|
||||
closes = [k['close'] for k in klines]
|
||||
highs = [k['high'] for k in klines]
|
||||
lows = [k['low'] for k in klines]
|
||||
volumes = [k.get('volume', 0) for k in klines]
|
||||
|
||||
atr_vals = calc_atr(highs, lows, closes, 14)
|
||||
current_atr = atr_vals[-1]
|
||||
if not current_atr:
|
||||
return None, 'ATR 失败'
|
||||
|
||||
vwaps = calc_vwap(closes, volumes)
|
||||
current_vwap = vwaps[-1]
|
||||
|
||||
current_price = quote.get('price') or quote.get('last_done', 0)
|
||||
if not current_price:
|
||||
return None, '价格缺失'
|
||||
|
||||
# ── 策略参数映射 ──
|
||||
if strategy == 'turtle_breakout':
|
||||
vol_sl_multi, vol_tp1_multi, vol_tp2_multi = 2.0, 4.0, 8.0
|
||||
eff_min_rr = min(min_rr, 1.0)
|
||||
use_vwap = False
|
||||
elif strategy == 'vwap_revert':
|
||||
vol_sl_multi, vol_tp1_multi, vol_tp2_multi = 1.0, 2.0, 3.0
|
||||
eff_min_rr = max(min_rr, 1.5)
|
||||
use_vwap = True
|
||||
elif strategy == 'early_bird':
|
||||
vol_sl_multi, vol_tp1_multi, vol_tp2_multi = 1.5, 2.0, 3.0
|
||||
eff_min_rr = min_rr
|
||||
use_vwap = False
|
||||
else: # rsi2_revert
|
||||
vol_sl_multi, vol_tp1_multi, vol_tp2_multi = 1.5, 2.0, 3.0
|
||||
eff_min_rr = min_rr
|
||||
use_vwap = True
|
||||
|
||||
eff_vwap = current_vwap if use_vwap else None
|
||||
return calc_exit_levels(
|
||||
entry=current_price,
|
||||
atr=current_atr,
|
||||
current_price=current_price,
|
||||
day_high=quote.get('high') or max(highs),
|
||||
day_low=quote.get('low') or min(lows),
|
||||
prev_high=max(highs),
|
||||
prev_low=min(lows),
|
||||
vwap=eff_vwap,
|
||||
side=side,
|
||||
min_rr=eff_min_rr,
|
||||
vol_sl_multi=vol_sl_multi,
|
||||
vol_tp1_multi=vol_tp1_multi,
|
||||
vol_tp2_multi=vol_tp2_multi,
|
||||
), strategy
|
||||
|
||||
|
||||
def format_cn_output(levels, change_pct, current_price, side, symbol, info, mode):
|
||||
"""A 股格式: ¥ 符号, 股息率"""
|
||||
name = info.get('name', symbol)
|
||||
div_yield = info.get('yield', 0)
|
||||
sector = info.get('sector', '')
|
||||
if mode == 'strict':
|
||||
return (
|
||||
f"\n📈 **{symbol}** {name} (股息率 {div_yield}% | {sector})\n"
|
||||
f"现价 ¥{current_price:.2f} ({change_pct:+.2f}%) | {side.upper()}\n"
|
||||
f"SL ¥{levels.sl:.2f}\n"
|
||||
f"TP1 ¥{levels.tp1:.2f}\n"
|
||||
f"TP2 ¥{levels.tp2:.2f}\n"
|
||||
f"R:R 1:{levels.rr_ratio:.2f} ✅"
|
||||
)
|
||||
elif mode == 'relaxed':
|
||||
return (
|
||||
f"\n📈 **{symbol}** {name} (股息率 {div_yield}% | {sector})\n"
|
||||
f"现价 ¥{current_price:.2f} ({change_pct:+.2f}%) | {side.upper()} [R:R 1.0 宽松]\n"
|
||||
f"SL ¥{levels.sl:.2f}\n"
|
||||
f"TP1 ¥{levels.tp1:.2f}\n"
|
||||
f"TP2 ¥{levels.tp2:.2f}\n"
|
||||
f"R:R 1:{levels.rr_ratio:.2f} ⚠️"
|
||||
)
|
||||
else: # atr_adj
|
||||
return (
|
||||
f"\n📈 **{symbol}** {name} (股息率 {div_yield}% | {sector})\n"
|
||||
f"现价 ¥{current_price:.2f} ({change_pct:+.2f}%) | {side.upper()} [ATR 调整]\n"
|
||||
f"SL ¥{levels.sl:.2f}\n"
|
||||
f"TP1 ¥{levels.tp1:.2f}\n"
|
||||
f"TP2 ¥{levels.tp2:.2f}\n"
|
||||
f"R:R 1:{levels.rr_ratio:.2f} ⚠️"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--top', type=int, default=5, help='top N (default 5)')
|
||||
ap.add_argument('--period', default='day',
|
||||
choices=['day', '5m', '15m', '1h'],
|
||||
help='K线周期 (default day)')
|
||||
ap.add_argument('--strategy', default='rsi2_revert',
|
||||
choices=['rsi2_revert', 'vwap_revert', 'early_bird', 'turtle_breakout'],
|
||||
help='策略 (default rsi2_revert, A 股推荐)')
|
||||
args = ap.parse_args()
|
||||
|
||||
# 过滤高股息
|
||||
candidates = {k: v for k, v in A_SHARE_POOL.items() if v.get('yield', 0) >= MIN_YIELD}
|
||||
top = list(candidates.items())[:args.top]
|
||||
|
||||
from datetime import date
|
||||
today = date.today().isoformat()
|
||||
|
||||
print(f"📊 A 股日内做T点位扫描 ({today}, top {args.top}, 策略={args.strategy})")
|
||||
print(f"📋 共扫描 {len(top)} 支\n")
|
||||
|
||||
output_lines = []
|
||||
|
||||
for symbol, info in top:
|
||||
print(f"--- {symbol} {info['name']} ---")
|
||||
|
||||
quote = get_quote(symbol)
|
||||
if not quote:
|
||||
print(f" ❌ quote 拉取失败")
|
||||
continue
|
||||
|
||||
klines = get_candlesticks(symbol, args.period, 30)
|
||||
if not klines:
|
||||
print(f" ❌ K线 拉取失败")
|
||||
continue
|
||||
|
||||
current_price = quote.get('price') or quote.get('last_done', 0)
|
||||
change_pct = quote.get('change_pct', 0)
|
||||
print(f" 现价: ¥{current_price:.2f} ({change_pct:+.2f}%)")
|
||||
|
||||
side = 'long' if change_pct > 0 else 'short'
|
||||
|
||||
levels, _ = calc_levels(symbol, klines, quote, side, min_rr=1.5, strategy=args.strategy)
|
||||
if levels:
|
||||
print(f" ✅ R:R 1.5 [{args.strategy}] → SL=¥{levels.sl:.2f} TP1=¥{levels.tp1:.2f} TP2=¥{levels.tp2:.2f} R:R=1:{levels.rr_ratio:.2f}")
|
||||
output_lines.append(format_cn_output(levels, change_pct, current_price, side, symbol, info, 'strict'))
|
||||
else:
|
||||
levels_relaxed, _ = calc_levels(symbol, klines, quote, side, min_rr=1.0, strategy=args.strategy)
|
||||
if levels_relaxed:
|
||||
print(f" ⚠️ R:R 1.5 否决, 1.0 通过 → R:R=1:{levels_relaxed.rr_ratio:.2f}")
|
||||
output_lines.append(format_cn_output(levels_relaxed, change_pct, current_price, side, symbol, info, 'relaxed'))
|
||||
else:
|
||||
closes = [k['close'] for k in klines]
|
||||
highs = [k['high'] for k in klines]
|
||||
lows = [k['low'] for k in klines]
|
||||
volumes = [k.get('volume', 0) for k in klines]
|
||||
atr_v = calc_atr(highs, lows, closes, 14)[-1]
|
||||
vwaps_v = calc_vwap(closes, volumes)
|
||||
levels_alt = calc_exit_levels(
|
||||
entry=current_price, atr=atr_v, current_price=current_price,
|
||||
day_high=max(highs), day_low=min(lows),
|
||||
prev_high=max(highs), prev_low=min(lows),
|
||||
vwap=vwaps_v[-1], side=side, min_rr=1.5,
|
||||
vol_sl_multi=1.5, vol_tp1_multi=3.0,
|
||||
)
|
||||
if levels_alt:
|
||||
print(f" ⚠️ ATR 调整 → R:R=1:{levels_alt.rr_ratio:.2f}")
|
||||
output_lines.append(format_cn_output(levels_alt, change_pct, current_price, side, symbol, info, 'atr_adj'))
|
||||
else:
|
||||
print(f" ❌ 全部场景否决")
|
||||
|
||||
print()
|
||||
|
||||
if output_lines:
|
||||
header = f"📊 A 股日内做T点位 ({today})\n⚠️ 仅参考, 不交易\n"
|
||||
print("\n=== QQ 推送内容 ===")
|
||||
print(header + "\n---\n".join(output_lines))
|
||||
else:
|
||||
print("\n💤 全部场景否决, 无输出")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,273 @@
|
||||
---
|
||||
name: hk-scan
|
||||
description: "港股日内做T点位扫描 — 拉候选池 top 5 + 实时 quote + 5min K, 算 SL/TP1/TP2 推 QQ (不交易, 仅参考)"
|
||||
---
|
||||
|
||||
"""
|
||||
港股日内做T点位扫描 (cron 模板)
|
||||
- 拉候选池 top 5 (artifact hk_intraday_latest.json)
|
||||
- 拉实时 quote + 5min K 线
|
||||
- 算 SL/TP1/TP2 用 calc_exit_levels()
|
||||
- 三级输出: ✅ R:R≥1.5 / ⚠️ R:R 1.0 / ❌ 否决
|
||||
- 推 QQ (origin delivery)
|
||||
|
||||
用法:
|
||||
python3 calc_hk_levels.py # 跑 (cron 默认)
|
||||
python3 calc_hk_levels.py --top 3 # 只看 top 3
|
||||
python3 calc_hk_levels.py --period 60m # 用 60min K
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加 strategy-management scripts 到 path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from exit_levels import calc_exit_levels
|
||||
from indicators import atr as calc_atr, vwap as calc_vwap
|
||||
|
||||
# === 常量 ===
|
||||
HERMES_HOME = '/home/openclaw'
|
||||
CANDIDATE_FILE = f'{HERMES_HOME}/.hermes/skills/trading/quant-factor-mining/artifacts/hk_intraday_latest.json'
|
||||
PROXYCHAINS = ['proxychains4', '-f', f'{HERMES_HOME}/.proxychains/proxychains.conf']
|
||||
LONGBRIDGE = ['/home/openclaw/.local/bin/longbridge', '--profile', 'lb_real']
|
||||
|
||||
|
||||
# === 长桥数据拉取 ===
|
||||
|
||||
def fetch_quote(symbol: str) -> dict:
|
||||
"""港美股 quote, JSON 格式 (港美都支持)"""
|
||||
result = subprocess.run(
|
||||
PROXYCHAINS + LONGBRIDGE + ['quote', symbol, '--json'],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
start = result.stdout.find('[')
|
||||
if start == -1:
|
||||
return {}
|
||||
try:
|
||||
return json.loads(result.stdout[start:])[0]
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def fetch_klines_hk(symbol: str, period: str = '5m', count: int = 30) -> list:
|
||||
"""
|
||||
港股 K 线 (表格 parser, 6 列, 不用 --json)
|
||||
- 分隔符 │ (U+2502), 不是 |
|
||||
- 表头中文: 时间/开盘/最高/最低/收盘/成交量
|
||||
- 时间格式: '2026-07-10 10:30' (空格分隔)
|
||||
- 数字带千分位逗号: '1,190,430'
|
||||
"""
|
||||
result = subprocess.run(
|
||||
PROXYCHAINS + LONGBRIDGE + ['candlesticks', symbol, period, '--count', str(count)],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
klines = []
|
||||
pattern = re.compile(
|
||||
r'│\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})\s*│'
|
||||
r'\s*([\d,.]+)\s*│\s*([\d,.]+)\s*│\s*([\d,.]+)\s*│\s*([\d,.]+)\s*│\s*([\d,.]+)\s*│'
|
||||
)
|
||||
for line in result.stdout.split('\n'):
|
||||
m = pattern.search(line)
|
||||
if m:
|
||||
ts, o, h, l, c, v = m.groups()
|
||||
def parse_num(s):
|
||||
return float(s.replace(',', ''))
|
||||
klines.append({
|
||||
'timestamp': ts.replace(' ', 'T'),
|
||||
'open': parse_num(o),
|
||||
'high': parse_num(h),
|
||||
'low': parse_num(l),
|
||||
'close': parse_num(c),
|
||||
'volume': parse_num(v),
|
||||
})
|
||||
return klines
|
||||
|
||||
|
||||
# === 业务逻辑 ===
|
||||
|
||||
def calc_levels(symbol: str, klines: list, quote: dict, side: str, min_rr: float,
|
||||
strategy: str = 'rsi2_revert'):
|
||||
"""
|
||||
strategy: 'rsi2_revert' | 'vwap_revert' | 'early_bird' | 'turtle_breakout'
|
||||
每个策略用不同的 vol_multi 参数组
|
||||
"""
|
||||
if not klines or not quote:
|
||||
return None, '数据缺失'
|
||||
closes = [k['close'] for k in klines]
|
||||
highs = [k['high'] for k in klines]
|
||||
lows = [k['low'] for k in klines]
|
||||
volumes = [k['volume'] for k in klines]
|
||||
|
||||
atr_vals = calc_atr(highs, lows, closes, 14)
|
||||
current_atr = atr_vals[-1]
|
||||
if not current_atr:
|
||||
return None, 'ATR 失败'
|
||||
|
||||
vwaps = calc_vwap(closes, volumes)
|
||||
current_vwap = vwaps[-1]
|
||||
|
||||
current_price = quote['last_done']
|
||||
|
||||
# ── 策略参数映射 ──
|
||||
if strategy == 'turtle_breakout':
|
||||
vol_sl_multi, vol_tp1_multi, vol_tp2_multi = 2.0, 4.0, 8.0
|
||||
eff_min_rr = min(min_rr, 1.0)
|
||||
use_vwap = False
|
||||
elif strategy == 'vwap_revert':
|
||||
vol_sl_multi, vol_tp1_multi, vol_tp2_multi = 1.0, 2.0, 3.0
|
||||
eff_min_rr = max(min_rr, 1.5)
|
||||
use_vwap = True
|
||||
elif strategy == 'early_bird':
|
||||
vol_sl_multi, vol_tp1_multi, vol_tp2_multi = 1.5, 2.0, 3.0
|
||||
eff_min_rr = min_rr
|
||||
use_vwap = False
|
||||
else: # rsi2_revert
|
||||
vol_sl_multi, vol_tp1_multi, vol_tp2_multi = 1.5, 2.0, 3.0
|
||||
eff_min_rr = min_rr
|
||||
use_vwap = True
|
||||
|
||||
eff_vwap = current_vwap if use_vwap else None
|
||||
return calc_exit_levels(
|
||||
entry=current_price,
|
||||
atr=current_atr,
|
||||
current_price=current_price,
|
||||
day_high=quote['high'],
|
||||
day_low=quote['low'],
|
||||
prev_high=max(highs),
|
||||
prev_low=min(lows),
|
||||
vwap=eff_vwap,
|
||||
side=side,
|
||||
min_rr=eff_min_rr,
|
||||
vol_sl_multi=vol_sl_multi,
|
||||
vol_tp1_multi=vol_tp1_multi,
|
||||
vol_tp2_multi=vol_tp2_multi,
|
||||
), strategy
|
||||
|
||||
|
||||
def format_qq_output(levels, change, current_price, side, symbol, score, adr, mode):
|
||||
"""格式化为 QQ 推送文本 (单条)"""
|
||||
if mode == 'strict':
|
||||
return (
|
||||
f"\n📈 **{symbol}** (score {score}, ADR {adr}%)\n"
|
||||
f"现价 ${current_price:.2f} ({change:+.2f}%) | {side.upper()}\n"
|
||||
f"SL ${levels.sl:.2f} ({levels.sl_method})\n"
|
||||
f"TP1 ${levels.tp1:.2f} ({levels.tp_method})\n"
|
||||
f"TP2 ${levels.tp2:.2f}\n"
|
||||
f"R:R 1:{levels.rr_ratio:.2f} ✅"
|
||||
)
|
||||
elif mode == 'relaxed':
|
||||
return (
|
||||
f"\n📈 **{symbol}** (score {score}, ADR {adr}%)\n"
|
||||
f"现价 ${current_price:.2f} ({change:+.2f}%) | {side.upper()} [R:R 1.0 宽松]\n"
|
||||
f"SL ${levels.sl:.2f}\n"
|
||||
f"TP1 ${levels.tp1:.2f}\n"
|
||||
f"TP2 ${levels.tp2:.2f}\n"
|
||||
f"R:R 1:{levels.rr_ratio:.2f} ⚠️"
|
||||
)
|
||||
else: # atr_adj
|
||||
return (
|
||||
f"\n📈 **{symbol}** (score {score}, ADR {adr}%)\n"
|
||||
f"现价 ${current_price:.2f} ({change:+.2f}%) | {side.upper()} [ATR 调整]\n"
|
||||
f"SL ${levels.sl:.2f}\n"
|
||||
f"TP1 ${levels.tp1:.2f}\n"
|
||||
f"TP2 ${levels.tp2:.2f}\n"
|
||||
f"R:R 1:{levels.rr_ratio:.2f} ⚠️"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--top', type=int, default=5, help='候选池 top N (default 5)')
|
||||
ap.add_argument('--period', default='5m', help='K 线周期 (default 5m)')
|
||||
ap.add_argument('--strategy', default='rsi2_revert',
|
||||
choices=['rsi2_revert', 'vwap_revert', 'early_bird', 'turtle_breakout'],
|
||||
help='策略 (default rsi2_revert)')
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.exists(CANDIDATE_FILE):
|
||||
print(f"[skip] 候选池不存在: {CANDIDATE_FILE}")
|
||||
return
|
||||
|
||||
with open(CANDIDATE_FILE) as f:
|
||||
candidate_data = json.load(f)
|
||||
|
||||
top = candidate_data.get('results', [])[:args.top]
|
||||
date = candidate_data.get('date', '?')[:10]
|
||||
|
||||
print(f"📊 港股日内做T点位扫描 (候选池 {date}, top {args.top})")
|
||||
print(f"📋 共扫描 {len(top)} 支\n")
|
||||
|
||||
output_lines = []
|
||||
|
||||
for entry in top:
|
||||
symbol = entry['ticker']
|
||||
score = entry['score']
|
||||
avg_adr = entry['avg_adr']
|
||||
|
||||
print(f"--- {symbol} (score {score}, ADR {avg_adr}%) ---")
|
||||
|
||||
quote = fetch_quote(symbol)
|
||||
if not quote:
|
||||
print(f" ❌ quote 拉取失败")
|
||||
continue
|
||||
|
||||
klines = fetch_klines_hk(symbol, args.period, 30)
|
||||
if not klines:
|
||||
print(f" ❌ K线 拉取失败")
|
||||
continue
|
||||
|
||||
current_price = quote['last_done']
|
||||
change = (current_price - quote['prev_close']) / quote['prev_close'] * 100
|
||||
print(f" 现价: ${current_price:.2f} ({change:+.2f}%)")
|
||||
|
||||
# 顺势方向
|
||||
side = 'long' if change > 0 else 'short'
|
||||
|
||||
# 三级尝试: 严格 / 宽松 / ATR 调整
|
||||
levels, _ = calc_levels(symbol, klines, quote, side, min_rr=1.5, strategy=args.strategy)
|
||||
if levels:
|
||||
print(f" ✅ R:R 1.5 [{args.strategy}] → SL=${levels.sl:.2f} TP1=${levels.tp1:.2f} TP2=${levels.tp2:.2f} R:R=1:{levels.rr_ratio:.2f}")
|
||||
output_lines.append(format_qq_output(levels, change, current_price, side, symbol, score, avg_adr, 'strict'))
|
||||
else:
|
||||
# 场景 B: 宽松
|
||||
levels_relaxed, _ = calc_levels(symbol, klines, quote, side, min_rr=1.0, strategy=args.strategy)
|
||||
if levels_relaxed:
|
||||
print(f" ⚠️ R:R 1.5 否决, 1.0 通过 → R:R=1:{levels_relaxed.rr_ratio:.2f}")
|
||||
output_lines.append(format_qq_output(levels_relaxed, change, current_price, side, symbol, score, avg_adr, 'relaxed'))
|
||||
else:
|
||||
# 场景 C: ATR 倍数调整 (SL=1.5 ATR, TP1=3.0 ATR)
|
||||
closes = [k['close'] for k in klines]
|
||||
highs = [k['high'] for k in klines]
|
||||
lows = [k['low'] for k in klines]
|
||||
volumes = [k['volume'] for k in klines]
|
||||
atr_v = calc_atr(highs, lows, closes, 14)[-1]
|
||||
vwaps = calc_vwap(closes, volumes)
|
||||
levels_alt = calc_exit_levels(
|
||||
entry=current_price, atr=atr_v, current_price=current_price,
|
||||
day_high=quote['high'], day_low=quote['low'],
|
||||
prev_high=max(highs), prev_low=min(lows),
|
||||
vwap=vwaps[-1], side=side, min_rr=1.5,
|
||||
vol_sl_multi=1.5, vol_tp1_multi=3.0,
|
||||
)
|
||||
if levels_alt:
|
||||
print(f" ⚠️ ATR 调整 → R:R=1:{levels_alt.rr_ratio:.2f}")
|
||||
output_lines.append(format_qq_output(levels_alt, change, current_price, side, symbol, score, avg_adr, 'atr_adj'))
|
||||
else:
|
||||
print(f" ❌ 全部场景否决")
|
||||
|
||||
print()
|
||||
|
||||
if output_lines:
|
||||
header = f"📊 港股日内做T点位 ({date})\n⚠️ 仅参考, 不交易\n"
|
||||
print("\n=== QQ 推送内容 ===")
|
||||
print(header + "\n---\n".join(output_lines))
|
||||
else:
|
||||
print("\n💤 全部场景否决, 无输出")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,257 @@
|
||||
---
|
||||
name: us-scan
|
||||
description: "美股日内做T点位扫描 — 拉候选池 top 5 + 实时 quote + 5min K, 算 SL/TP1/TP2 推 QQ (不交易, 仅参考)"
|
||||
---
|
||||
|
||||
"""
|
||||
美股日内做T点位扫描 (cron 模板)
|
||||
- 拉候选池 top 5 (artifact us_intraday_latest.json)
|
||||
- 拉实时 quote + 5min K 线 (JSON 输出)
|
||||
- 算 SL/TP1/TP2 用 calc_exit_levels()
|
||||
- 三级输出: ✅ R:R≥1.5 / ⚠️ R:R 1.0 / ❌ 否决
|
||||
- 推 QQ (origin delivery)
|
||||
|
||||
用法:
|
||||
python3 calc_us_levels.py # 跑 (cron 默认)
|
||||
python3 calc_us_levels.py --top 3 # 只看 top 3
|
||||
python3 calc_us_levels.py --period 60m # 用 60min K
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from exit_levels import calc_exit_levels
|
||||
from indicators import atr as calc_atr, vwap as calc_vwap
|
||||
|
||||
# === 常量 ===
|
||||
HERMES_HOME = '/home/openclaw'
|
||||
CANDIDATE_FILE = f'{HERMES_HOME}/.hermes/skills/trading/quant-factor-mining/artifacts/us_intraday_latest.json'
|
||||
PROXYCHAINS = ['proxychains4', '-f', f'{HERMES_HOME}/.proxychains/proxychains.conf']
|
||||
LONGBRIDGE = ['/home/openclaw/.local/bin/longbridge', '--profile', 'lb_real']
|
||||
|
||||
|
||||
# === 长桥数据拉取 (美股版) ===
|
||||
|
||||
def fetch_quote(symbol: str) -> dict:
|
||||
"""美股 quote, JSON"""
|
||||
result = subprocess.run(
|
||||
PROXYCHAINS + LONGBRIDGE + ['quote', symbol, '--json'],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
start = result.stdout.find('[')
|
||||
if start == -1:
|
||||
return {}
|
||||
try:
|
||||
return json.loads(result.stdout[start:])[0]
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def fetch_klines_us(symbol: str, period: str = '5m', count: int = 30) -> list:
|
||||
"""
|
||||
美股 K 线 (JSON, 7 列含 turnover)
|
||||
- 支持 --json
|
||||
- stdout 拼接表格提示 + JSON → find('[') 切
|
||||
- 时间格式: '2026-07-16T03:55:00' (T 分隔)
|
||||
- 数字无千分位
|
||||
"""
|
||||
result = subprocess.run(
|
||||
PROXYCHAINS + LONGBRIDGE + ['candlesticks', symbol, period, '--count', str(count), '--json'],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
# 长桥把表格提示 (USOption/HK) + JSON 拼一起
|
||||
start = result.stdout.find('[')
|
||||
if start == -1:
|
||||
return []
|
||||
try:
|
||||
data = json.loads(result.stdout[start:])
|
||||
return [k for k in data if 'timestamp' in k and 'close' in k]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
# === 业务逻辑 (与 calc_hk_levels.py 完全一致, 只是 klines 函数不同) ===
|
||||
|
||||
def calc_levels(symbol: str, klines: list, quote: dict, side: str, min_rr: float,
|
||||
strategy: str = 'rsi2_revert'):
|
||||
"""
|
||||
strategy: 'rsi2_revert' | 'vwap_revert' | 'early_bird' | 'turtle_breakout'
|
||||
每个策略用不同的 vol_multi 参数组
|
||||
"""
|
||||
if not klines or not quote:
|
||||
return None, '数据缺失'
|
||||
closes = [k['close'] for k in klines]
|
||||
highs = [k['high'] for k in klines]
|
||||
lows = [k['low'] for k in klines]
|
||||
volumes = [k.get('volume', 0) for k in klines]
|
||||
atr_vals = calc_atr(highs, lows, closes, 14)
|
||||
current_atr = atr_vals[-1]
|
||||
if not current_atr:
|
||||
return None, 'ATR 失败'
|
||||
vwaps = calc_vwap(closes, volumes)
|
||||
current_vwap = vwaps[-1]
|
||||
current_price = quote['last_done']
|
||||
|
||||
# ── 策略参数映射 ──
|
||||
if strategy == 'turtle_breakout':
|
||||
# 海龟: SL=2ATR, TP1=4ATR(1R), TP2=8ATR(2R), 更宽松 min_rr=1.0
|
||||
vol_sl_multi = 2.0
|
||||
vol_tp1_multi = 4.0
|
||||
vol_tp2_multi = 8.0
|
||||
eff_min_rr = min(min_rr, 1.0)
|
||||
use_vwap = False # 海龟不看 VWAP
|
||||
elif strategy == 'vwap_revert':
|
||||
# VWAP 回归: SL=1σ, TP1=VWAP±0.05%, TP2=2ATR
|
||||
vol_sl_multi = 1.0
|
||||
vol_tp1_multi = 2.0
|
||||
vol_tp2_multi = 3.0
|
||||
eff_min_rr = max(min_rr, 1.5)
|
||||
use_vwap = True
|
||||
elif strategy == 'early_bird':
|
||||
# 开盘缺口: SL=1.5ATR, TP1=2ATR, TP2=3ATR
|
||||
vol_sl_multi = 1.5
|
||||
vol_tp1_multi = 2.0
|
||||
vol_tp2_multi = 3.0
|
||||
eff_min_rr = min_rr
|
||||
use_vwap = False
|
||||
else:
|
||||
# rsi2_revert (默认): SL=1.5, TP1=2.0, TP2=3.0
|
||||
vol_sl_multi = 1.5
|
||||
vol_tp1_multi = 2.0
|
||||
vol_tp2_multi = 3.0
|
||||
eff_min_rr = min_rr
|
||||
use_vwap = True
|
||||
|
||||
eff_vwap = current_vwap if use_vwap else None
|
||||
return calc_exit_levels(
|
||||
entry=current_price, atr=current_atr, current_price=current_price,
|
||||
day_high=quote['high'], day_low=quote['low'],
|
||||
prev_high=max(highs), prev_low=min(lows),
|
||||
vwap=eff_vwap, side=side, min_rr=eff_min_rr,
|
||||
vol_sl_multi=vol_sl_multi, vol_tp1_multi=vol_tp1_multi, vol_tp2_multi=vol_tp2_multi,
|
||||
), strategy
|
||||
|
||||
|
||||
def format_qq_output(levels, change, current_price, side, symbol, score, adr, mode):
|
||||
if mode == 'strict':
|
||||
return (
|
||||
f"\n📈 **{symbol}** (score {score}, ADR {adr}%)\n"
|
||||
f"现价 ${current_price:.2f} ({change:+.2f}%) | {side.upper()}\n"
|
||||
f"SL ${levels.sl:.2f} ({levels.sl_method})\n"
|
||||
f"TP1 ${levels.tp1:.2f} ({levels.tp_method})\n"
|
||||
f"TP2 ${levels.tp2:.2f}\n"
|
||||
f"R:R 1:{levels.rr_ratio:.2f} ✅"
|
||||
)
|
||||
elif mode == 'relaxed':
|
||||
return (
|
||||
f"\n📈 **{symbol}** (score {score}, ADR {adr}%)\n"
|
||||
f"现价 ${current_price:.2f} ({change:+.2f}%) | {side.upper()} [R:R 1.0 宽松]\n"
|
||||
f"SL ${levels.sl:.2f}\n"
|
||||
f"TP1 ${levels.tp1:.2f}\n"
|
||||
f"TP2 ${levels.tp2:.2f}\n"
|
||||
f"R:R 1:{levels.rr_ratio:.2f} ⚠️"
|
||||
)
|
||||
else:
|
||||
return (
|
||||
f"\n📈 **{symbol}** (score {score}, ADR {adr}%)\n"
|
||||
f"现价 ${current_price:.2f} ({change:+.2f}%) | {side.upper()} [ATR 调整]\n"
|
||||
f"SL ${levels.sl:.2f}\n"
|
||||
f"TP1 ${levels.tp1:.2f}\n"
|
||||
f"TP2 ${levels.tp2:.2f}\n"
|
||||
f"R:R 1:{levels.rr_ratio:.2f} ⚠️"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--top', type=int, default=5)
|
||||
ap.add_argument('--period', default='5m')
|
||||
ap.add_argument('--strategy', default='rsi2_revert',
|
||||
choices=['rsi2_revert', 'vwap_revert', 'early_bird', 'turtle_breakout'],
|
||||
help='策略 (default rsi2_revert)')
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.exists(CANDIDATE_FILE):
|
||||
print(f"[skip] 候选池不存在: {CANDIDATE_FILE}")
|
||||
return
|
||||
|
||||
with open(CANDIDATE_FILE) as f:
|
||||
candidate_data = json.load(f)
|
||||
|
||||
top = candidate_data.get('results', [])[:args.top]
|
||||
date = candidate_data.get('date', '?')[:10]
|
||||
|
||||
print(f"📊 美股日内做T点位扫描 (候选池 {date}, top {args.top})")
|
||||
print(f"📋 共扫描 {len(top)} 支\n")
|
||||
|
||||
output_lines = []
|
||||
|
||||
for entry in top:
|
||||
symbol = entry['ticker']
|
||||
score = entry['score']
|
||||
avg_adr = entry['avg_adr']
|
||||
|
||||
print(f"--- {symbol} (score {score}, ADR {avg_adr}%) ---")
|
||||
|
||||
quote = fetch_quote(symbol)
|
||||
if not quote:
|
||||
print(f" ❌ quote 拉取失败")
|
||||
continue
|
||||
|
||||
klines = fetch_klines_us(symbol, args.period, 30)
|
||||
if not klines:
|
||||
print(f" ❌ K线 拉取失败")
|
||||
continue
|
||||
|
||||
current_price = quote['last_done']
|
||||
change = (current_price - quote['prev_close']) / quote['prev_close'] * 100
|
||||
print(f" 现价: ${current_price:.2f} ({change:+.2f}%)")
|
||||
|
||||
side = 'long' if change > 0 else 'short'
|
||||
|
||||
levels, _ = calc_levels(symbol, klines, quote, side, min_rr=1.5, strategy=args.strategy)
|
||||
if levels:
|
||||
print(f" ✅ R:R 1.5 [{args.strategy}] → SL=${levels.sl:.2f} TP1=${levels.tp1:.2f} TP2=${levels.tp2:.2f} R:R=1:{levels.rr_ratio:.2f}")
|
||||
output_lines.append(format_qq_output(levels, change, current_price, side, symbol, score, avg_adr, 'strict'))
|
||||
else:
|
||||
levels_relaxed, _ = calc_levels(symbol, klines, quote, side, min_rr=1.0, strategy=args.strategy)
|
||||
if levels_relaxed:
|
||||
print(f" ⚠️ R:R 1.5 否决, 1.0 通过 → R:R=1:{levels_relaxed.rr_ratio:.2f}")
|
||||
output_lines.append(format_qq_output(levels_relaxed, change, current_price, side, symbol, score, avg_adr, 'relaxed'))
|
||||
else:
|
||||
# ATR 调整
|
||||
closes = [k['close'] for k in klines]
|
||||
highs = [k['high'] for k in klines]
|
||||
lows = [k['low'] for k in klines]
|
||||
volumes = [k.get('volume', 0) for k in klines]
|
||||
atr_v = calc_atr(highs, lows, closes, 14)[-1]
|
||||
vwaps = calc_vwap(closes, volumes)
|
||||
levels_alt = calc_exit_levels(
|
||||
entry=current_price, atr=atr_v, current_price=current_price,
|
||||
day_high=quote['high'], day_low=quote['low'],
|
||||
prev_high=max(highs), prev_low=min(lows),
|
||||
vwap=vwaps[-1], side=side, min_rr=1.5,
|
||||
vol_sl_multi=1.5, vol_tp1_multi=3.0,
|
||||
)
|
||||
if levels_alt:
|
||||
print(f" ⚠️ ATR 调整 → R:R=1:{levels_alt.rr_ratio:.2f}")
|
||||
output_lines.append(format_qq_output(levels_alt, change, current_price, side, symbol, score, avg_adr, 'atr_adj'))
|
||||
else:
|
||||
print(f" ❌ 全部场景否决")
|
||||
|
||||
print()
|
||||
|
||||
if output_lines:
|
||||
header = f"📊 美股日内做T点位 ({date})\n⚠️ 仅参考, 不交易\n"
|
||||
print("\n=== QQ 推送内容 ===")
|
||||
print(header + "\n---\n".join(output_lines))
|
||||
else:
|
||||
print("\n💤 全部场景否决, 无输出")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user