#!/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=== 完成 ===")