【备份】cron 已迁到 ~/.hermes/scripts/symlink, 旧副本 ~/.hermes/scripts/crypto/ 和 stocks/ 即将删, 先备份
- crypto-t-monitor/scripts/backtest.py + okx_t_monitor.py
- intraday-trading/scripts/{hk,us}_intraday_cli.py + hk_intraday_cli_runner.sh
- strategy-management/scripts/backtest.py (与 crypto-t-monitor 重复, 备份占位)
【未删本地】等用户确认
279 lines
11 KiB
Python
Executable File
279 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""港股日内交易监控+自动下单 - CLI 路径"""
|
|
import os, sys, json, time
|
|
from datetime import datetime
|
|
|
|
# 强制 CLI 路径走 .com 海外域 (避免 602315)
|
|
os.environ['LONGBRIDGE_HTTP_URL'] = 'https://openapi.longbridge.com'
|
|
os.environ['LONGBRIDGE_REGION'] = 'ap'
|
|
os.environ['LONGBRIDGE_TRADE_ENABLED'] = 'true'
|
|
|
|
# 替换 longport 模块为 CLI helper (Python SDK 走 cn 域会 602315)
|
|
sys.path.insert(0, '/home/openclaw/.hermes/scripts')
|
|
import longbridge_cli_helper as _helper
|
|
_fake_longport = type(sys)('longport')
|
|
_fake_longport.openapi = _helper
|
|
sys.modules['longport'] = _fake_longport
|
|
sys.modules['longport.openapi'] = _helper
|
|
from longport import openapi # 现在 openapi 实际是 helper
|
|
|
|
# 剩余代码跟原版一致
|
|
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', '')
|
|
|
|
ctx = openapi.QuoteContext(config=None)
|
|
|
|
# === 余额 + 持仓 ===
|
|
bals = openapi.account_balance()
|
|
hkd_cash = 0
|
|
usd_cash = 0
|
|
if bals:
|
|
for b in bals:
|
|
cur = str(b.currency).upper()
|
|
cash = float(getattr(b, 'cash_available', 0) or 0)
|
|
if cash <= 0:
|
|
cash = float(getattr(b, 'buy_power', 0) or 0)
|
|
if 'USD' in cur:
|
|
usd_cash += cash
|
|
elif 'HKD' in cur:
|
|
hkd_cash += cash
|
|
print(f"💰 HKD cash: {hkd_cash:.0f} | USD cash: {usd_cash:.2f}")
|
|
print(f"💰 单笔仓位 (HKD): {hkd_cash*0.25:.0f} | (USD): {usd_cash*0.25:.2f}")
|
|
|
|
# 持仓
|
|
held_symbols = set()
|
|
positions = openapi.stock_positions()
|
|
for ch in positions.channels:
|
|
for p in ch.positions:
|
|
held_symbols.add(p.symbol)
|
|
print(f" 持仓: {p.symbol} {p.quantity}股 @ {p.cost_price}")
|
|
|
|
# === 读取盘前候选 ===
|
|
screen_file = os.path.expanduser('~/.hermes/skills/trading/quant-factor-mining/artifacts/hk_intraday_latest.json')
|
|
if not os.path.exists(screen_file):
|
|
print("❌ 未找到盘前筛选结果")
|
|
sys.exit(1)
|
|
|
|
with open(screen_file) as f:
|
|
screen = json.load(f)
|
|
|
|
# 取 TOP 3
|
|
candidates = [r for r in screen.get('results', [])[:3]]
|
|
print(f"\n🎯 监控标的:")
|
|
for c in candidates:
|
|
print(f" {c['ticker']}: 评分 {c['score']:.1f} | ADR {c['avg_adr']:.2f}%")
|
|
|
|
# === 读取入场记录 ===
|
|
entry_file = os.path.expanduser('~/.hermes/trading/hk_intraday_entries.json')
|
|
entries = {}
|
|
if os.path.exists(entry_file):
|
|
try:
|
|
entries = json.load(open(entry_file))
|
|
except:
|
|
entries = {}
|
|
|
|
# === 遍历每个候选, 检查入场/出场信号 ===
|
|
for c in candidates:
|
|
ticker = c['ticker']
|
|
try:
|
|
q = ctx.quote([ticker])[0]
|
|
current = float(q.last_done)
|
|
except Exception as e:
|
|
print(f"⏳ {ticker}: 行情获取失败: {e}")
|
|
continue
|
|
|
|
# 简化版信号: 价格突破 SMA5 且 SMA5 > SMA10 → 入场
|
|
try:
|
|
cs = ctx.candlesticks(ticker, openapi.Period.Day, 30, openapi.AdjustType.ForwardAdjust)
|
|
closes = [float(c2.close) for c2 in cs]
|
|
sma5 = sum(closes[-5:]) / 5
|
|
sma10 = sum(closes[-10:]) / 10
|
|
except Exception as e:
|
|
print(f"⏳ {ticker}: K线失败: {e}")
|
|
continue
|
|
|
|
if ticker in held_symbols:
|
|
print(f"⏳ {ticker}: 已有持仓,跳过入场检查 | 现价 {current:.2f}")
|
|
continue
|
|
|
|
# 如果已有日内入场记录, 也跳过(防止重复下单)
|
|
if ticker in entries:
|
|
# 检查出场信号
|
|
entry = entries[ticker]
|
|
e_shares = entry.get('shares', 0)
|
|
e_order_id = entry.get('order_id', '')
|
|
|
|
if not e_order_id:
|
|
print(f"⚠️ {ticker}: 有入场记录但无订单ID, 跳过")
|
|
continue
|
|
|
|
if current <= entry['stop_loss']:
|
|
print(f"\n🛑 {ticker} 触发止损! {current:.2f} <= {entry['stop_loss']}")
|
|
try:
|
|
openapi.submit_order(
|
|
symbol=ticker, order_type=openapi.OrderType.MO,
|
|
side=openapi.OrderSide.Sell,
|
|
submitted_quantity=e_shares,
|
|
time_in_force=openapi.TimeInForceType.Day,
|
|
)
|
|
print(f" ✅ 止损平仓: 卖 {e_shares}股 @ 市价")
|
|
del entries[ticker]
|
|
with open(entry_file, 'w') as f:
|
|
json.dump(entries, f, indent=2)
|
|
except Exception as e:
|
|
print(f" ❌ 平仓失败: {e}")
|
|
elif current >= entry['take_profit']:
|
|
print(f"\n🎯 {ticker} 触发止盈! {current:.2f} >= {entry['take_profit']}")
|
|
try:
|
|
openapi.submit_order(
|
|
symbol=ticker, order_type=openapi.OrderType.MO,
|
|
side=openapi.OrderSide.Sell,
|
|
submitted_quantity=e_shares,
|
|
time_in_force=openapi.TimeInForceType.Day,
|
|
)
|
|
print(f" ✅ 止盈平仓: 卖 {e_shares}股 @ 市价")
|
|
del entries[ticker]
|
|
with open(entry_file, 'w') as f:
|
|
json.dump(entries, f, indent=2)
|
|
except Exception as e:
|
|
print(f" ❌ 平仓失败: {e}")
|
|
else:
|
|
print(f"⏳ {ticker}: 已入场,持仓中 | 现价 {current:.2f} | 止损 {entry['stop_loss']} | 止盈 {entry['take_profit']}")
|
|
continue
|
|
|
|
# === 入场信号 ===
|
|
if current > sma5 > sma10 and current > closes[-2]:
|
|
# 计算仓位: 20% cash (按标的货币), 按 lot_size 取整
|
|
price = round(current, 2)
|
|
# 港股 lot_size 可能 100/200/500/1000/2000 (ticker 依赖), 美股=1
|
|
lot_size = openapi.get_lot_size(ticker) if hasattr(openapi, 'get_lot_size') else 100
|
|
# 选对应货币的 cash
|
|
cash = hkd_cash # 港股账户默认 HKD
|
|
target_value = cash * 0.20
|
|
shares = int(target_value / price / lot_size) * lot_size
|
|
if shares < lot_size:
|
|
print(f"⏳ {ticker}: 信号但余额不足 (需要{lot_size}股 @ {price})")
|
|
continue
|
|
|
|
stop_loss = round(price * 0.985, 2)
|
|
take_profit = round(price * 1.025, 2)
|
|
|
|
# 调整下单价格到合法范围 (港股 9 档保护规则)
|
|
adjusted_price = openapi.adjust_price_for_order(ticker, price, 'buy') if hasattr(openapi, 'adjust_price_for_order') else price
|
|
if abs(adjusted_price - price) > 0.05:
|
|
print(f" ⚠️ 价格调整: {price} → {adjusted_price} (盘口约束)")
|
|
|
|
# 基于 adjusted_price 重新算止损止盈
|
|
stop_loss = round(adjusted_price * 0.985, 2)
|
|
take_profit = round(adjusted_price * 1.025, 2)
|
|
|
|
print(f"\n🔔 {ticker} 入场信号!")
|
|
print(f" 方向: 做多 | 现价 {current:.2f} | SMA5 {sma5:.2f}")
|
|
print(f" 止损: {stop_loss} | 止盈: {take_profit} | 股数: {shares}")
|
|
|
|
# 自动下单
|
|
try:
|
|
resp = openapi.submit_order(
|
|
symbol=ticker, order_type=openapi.OrderType.LO,
|
|
side=openapi.OrderSide.Buy,
|
|
submitted_quantity=shares,
|
|
time_in_force=openapi.TimeInForceType.Day,
|
|
submitted_price=adjusted_price,
|
|
)
|
|
order_id = resp.order_id
|
|
print(f" ⏳ 已提交: {order_id}")
|
|
|
|
# 反查 status (700 RMB 教训)
|
|
import time as _t
|
|
status = 'Unknown'
|
|
detail = None
|
|
for retry in range(3):
|
|
_t.sleep(0.5)
|
|
try:
|
|
detail = openapi.order_detail(order_id)
|
|
status = str(detail.status).split('.')[-1] if detail else 'Unknown'
|
|
if status not in ('New', 'NotReported'):
|
|
break
|
|
except Exception:
|
|
continue
|
|
|
|
if status == 'Filled':
|
|
print(f" ✅ 成交: {order_id}")
|
|
elif status == 'Rejected':
|
|
print(f" ❌ 被拒: {order_id} | 跳过")
|
|
continue
|
|
elif status == 'Canceled':
|
|
print(f" 🚫 已撤: {order_id}")
|
|
continue
|
|
else:
|
|
print(f" ⚠️ 已挂单未成交: {order_id} (status={status})")
|
|
|
|
# 记录 (用 adjusted_price 作为 entry_price)
|
|
entries[ticker] = {
|
|
'side': 'buy',
|
|
'entry_price': adjusted_price,
|
|
'stop_loss': stop_loss,
|
|
'take_profit': take_profit,
|
|
'shares': shares,
|
|
'order_id': order_id,
|
|
'time': datetime.now().isoformat(),
|
|
}
|
|
os.makedirs(os.path.dirname(entry_file), exist_ok=True)
|
|
with open(entry_file, 'w') as f:
|
|
json.dump(entries, f, indent=2)
|
|
except Exception as e:
|
|
print(f" ❌ 下单失败: {e}")
|
|
|
|
elif ticker in entries:
|
|
# === 出场信号 ===
|
|
entry = entries[ticker]
|
|
e_shares = entry.get('shares', 0)
|
|
e_order_id = entry.get('order_id', '')
|
|
|
|
if not e_order_id:
|
|
print(f"⚠️ {ticker}: 无订单ID, 跳过")
|
|
continue
|
|
|
|
if current <= entry['stop_loss']:
|
|
print(f"🛑 {ticker} 止损! {current:.2f} <= {entry['stop_loss']}")
|
|
try:
|
|
openapi.submit_order(
|
|
symbol=ticker, order_type=openapi.OrderType.MO,
|
|
side=openapi.OrderSide.Sell,
|
|
submitted_quantity=e_shares,
|
|
time_in_force=openapi.TimeInForceType.Day,
|
|
)
|
|
print(f" ✅ 止损平仓: 卖 {e_shares}股")
|
|
del entries[ticker]
|
|
with open(entry_file, 'w') as f:
|
|
json.dump(entries, f, indent=2)
|
|
except Exception as e:
|
|
print(f" ❌ 平仓失败: {e}")
|
|
|
|
elif current >= entry['take_profit']:
|
|
print(f"🎯 {ticker} 止盈! {current:.2f} >= {entry['take_profit']}")
|
|
try:
|
|
openapi.submit_order(
|
|
symbol=ticker, order_type=openapi.OrderType.MO,
|
|
side=openapi.OrderSide.Sell,
|
|
submitted_quantity=e_shares,
|
|
time_in_force=openapi.TimeInForceType.Day,
|
|
)
|
|
print(f" ✅ 止盈平仓: 卖 {e_shares}股")
|
|
del entries[ticker]
|
|
with open(entry_file, 'w') as f:
|
|
json.dump(entries, f, indent=2)
|
|
except Exception as e:
|
|
print(f" ❌ 平仓失败: {e}")
|
|
else:
|
|
print(f"⏳ {ticker}: 等待信号 | 现价 {current:.2f} | SMA5 {sma5:.2f} | SMA10 {sma10:.2f}")
|
|
|
|
print("\n=== 完成 ===") |