Initial commit: Hermes Agent skills collection
- Trading skills (OKX, dividend, lottery, quantitative) - Creative skills (ASCII art, diagrams, video) - Development skills (GitHub, debugging, TDD) - Research skills (arXiv, blog monitoring) - Productivity skills (email, documents, notes) - MCP integration skills - Custom user skills
This commit is contained in:
@@ -0,0 +1,494 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
交易信号处理器(no_agent模式):
|
||||
1. 解析TG信号文本
|
||||
2. 调advisor脚本获取正确金额
|
||||
3. 格式化含📐完整模板
|
||||
4. 推QQ
|
||||
5. 信号去重/合并
|
||||
|
||||
用法: python3 process_signal.py "信号文本"
|
||||
或: echo "信号文本" | python3 process_signal.py
|
||||
|
||||
cron模式: 作为no_agent cron job的script使用
|
||||
"""
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import subprocess
|
||||
import sqlite3
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
SKILL_DIR = Path.home() / ".hermes/skills/trading/okx-auto-position"
|
||||
ADVISOR = SKILL_DIR / "scripts" / "okx_position_advisor.py"
|
||||
QQ_PUSH = Path.home() / ".hermes/scripts/push_to_qq.sh"
|
||||
SIGNAL_DB = Path.home() / ".hermes/trading/signal_history.db"
|
||||
DEDUP_DB = Path.home() / ".hermes/trading/signal_dedup.db"
|
||||
|
||||
# Import signal tracker
|
||||
sys.path.insert(0, str(SKILL_DIR / "scripts"))
|
||||
from signal_tracker import format_comparison, record_signal as _tracker_record, record_confirmed, format_trader_rating
|
||||
|
||||
# ─── 解析 ────────────────────────────────────────────────────────────────
|
||||
|
||||
def parse_signal(text):
|
||||
"""从TG信号文本提取关键字段"""
|
||||
fields = {}
|
||||
|
||||
# 交易员
|
||||
m = re.search(r'【([^】]{1,20})】', text)
|
||||
if m:
|
||||
fields['trader'] = m.group(1)
|
||||
|
||||
# 字段映射
|
||||
extractors = {
|
||||
'symbol': r'【币种】\s*[::]?\s*(\S+)',
|
||||
'side': r'【方向】\s*[::]?\s*(做多|做空)',
|
||||
'leverage':r'【杠杆】\s*[::]?\s*(\d+)',
|
||||
'size': r'【仓位大小】\s*[::]?\s*([\d,.]+)',
|
||||
'value': r'【仓位价值】\s*[::]?\s*\$?\s*([\d,.]+)',
|
||||
'entry': r'【开仓价】\s*[::]?\s*([\d,.]+)',
|
||||
'current': r'【当前价】\s*[::]?\s*([\d,.]+)',
|
||||
'pnl': r'【未实现盈亏】\s*[::]?\s*([-\d,.]+)',
|
||||
'margin': r'【保证金】\s*[::]?\s*\$?\s*([\d,.]+)',
|
||||
}
|
||||
|
||||
for key, pattern in extractors.items():
|
||||
m = re.search(pattern, text)
|
||||
if m:
|
||||
fields[key] = m.group(1).replace(',', '')
|
||||
|
||||
# 清理symbol
|
||||
if 'symbol' in fields:
|
||||
sym = fields['symbol']
|
||||
sym = re.sub(r'\|.*$', '', sym) # 去掉 |永续|10x
|
||||
sym = sym.replace('USDT', '').strip()
|
||||
fields['symbol'] = sym
|
||||
|
||||
# 方向转英文
|
||||
if fields.get('side', '').startswith('做多'):
|
||||
fields['side_en'] = 'long'
|
||||
else:
|
||||
fields['side_en'] = 'short'
|
||||
|
||||
return fields
|
||||
|
||||
# ─── 去重 ────────────────────────────────────────────────────────────────
|
||||
|
||||
def init_dedup_db():
|
||||
conn = sqlite3.connect(str(DEDUP_DB))
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS recent_signals (
|
||||
id TEXT PRIMARY KEY,
|
||||
symbol TEXT,
|
||||
trader TEXT,
|
||||
timestamp REAL,
|
||||
raw_text TEXT
|
||||
)
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS processed (
|
||||
msg_hash TEXT PRIMARY KEY,
|
||||
processed_at REAL
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
return conn
|
||||
|
||||
def is_duplicate(conn, text, symbol, trader):
|
||||
"""检查是否重复信号(同交易员同币种2分钟内)"""
|
||||
msg_hash = hashlib.md5(text.encode()).hexdigest()
|
||||
|
||||
# 检查完全相同的消息
|
||||
row = conn.execute(
|
||||
"SELECT 1 FROM processed WHERE msg_hash = ?", (msg_hash,)
|
||||
).fetchone()
|
||||
if row:
|
||||
return True
|
||||
|
||||
# 检查同交易员同币种2分钟内的信号
|
||||
cutoff = datetime.now().timestamp() - 120 # 2分钟
|
||||
row = conn.execute(
|
||||
"""SELECT 1 FROM recent_signals
|
||||
WHERE symbol = ? AND trader = ? AND timestamp > ?
|
||||
ORDER BY timestamp DESC LIMIT 1""",
|
||||
(symbol, trader, cutoff)
|
||||
).fetchone()
|
||||
|
||||
return row is not None
|
||||
|
||||
def record_signal(conn, text, symbol, trader):
|
||||
"""记录信号用于去重"""
|
||||
msg_hash = hashlib.md5(text.encode()).hexdigest()
|
||||
now = datetime.now().timestamp()
|
||||
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO processed (msg_hash, processed_at) VALUES (?, ?)",
|
||||
(msg_hash, now)
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO recent_signals (id, symbol, trader, timestamp, raw_text) VALUES (?, ?, ?, ?, ?)",
|
||||
(msg_hash, symbol, trader, now, text[:500])
|
||||
)
|
||||
|
||||
# 清理1小时前的记录
|
||||
cutoff = now - 3600
|
||||
conn.execute("DELETE FROM recent_signals WHERE timestamp < ?", (cutoff,))
|
||||
conn.execute("DELETE FROM processed WHERE processed_at < ?", (cutoff,))
|
||||
conn.commit()
|
||||
|
||||
# ─── Advisor ──────────────────────────────────────────────────────────────
|
||||
|
||||
def run_advisor(symbol, side, leverage):
|
||||
"""调advisor脚本获取正确数据"""
|
||||
cmd = [
|
||||
'python3', str(ADVISOR),
|
||||
'--symbol', symbol,
|
||||
'--side', side,
|
||||
'--leverage', str(leverage),
|
||||
'--json'
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=30,
|
||||
cwd=str(ADVISOR.parent)
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return json.loads(result.stdout)
|
||||
else:
|
||||
return {'error': result.stderr.strip()[:200]}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {'error': 'advisor超时'}
|
||||
except json.JSONDecodeError:
|
||||
return {'error': 'advisor输出非JSON'}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
# ─── 分类 ────────────────────────────────────────────────────────────────
|
||||
|
||||
def classify_signal(fields):
|
||||
"""判断信号类型:加仓/新开仓/减仓/平仓"""
|
||||
text = fields.get('_raw', '')
|
||||
|
||||
# 平仓信号
|
||||
if '平仓' in text or '止盈' in text or '止损' in text:
|
||||
return 'close'
|
||||
|
||||
# 减仓信号
|
||||
pnl = float(fields.get('pnl', '0').replace('+', ''))
|
||||
if '减仓' in text or (pnl < 0 and '减' in text):
|
||||
return 'reduce'
|
||||
|
||||
# 默认为新开仓或加仓(由advisor判断)
|
||||
return 'open'
|
||||
|
||||
# ─── 格式化 ──────────────────────────────────────────────────────────────
|
||||
|
||||
def format_message(fields, rec, signal_type):
|
||||
"""格式化完整推送消息"""
|
||||
if 'error' in rec:
|
||||
return f"⚠️ advisor错误: {rec['error']}"
|
||||
|
||||
symbol = fields.get('symbol', '?')
|
||||
side_cn = fields.get('side', '做多')
|
||||
emoji = '🟩' if fields.get('side_en') == 'long' else '🟥'
|
||||
leverage = fields.get('leverage', '10')
|
||||
trader = fields.get('trader', '?')
|
||||
size = fields.get('size', '?')
|
||||
value = fields.get('value', '?')
|
||||
entry_price = fields.get('entry', '?')
|
||||
pnl_str = fields.get('pnl', '0')
|
||||
pnl = float(pnl_str.replace('+', '')) if pnl_str else 0
|
||||
current = rec.get('price', fields.get('current', '?'))
|
||||
|
||||
pnl_emoji = '🔥' if pnl > 0 else '🔴'
|
||||
pnl_sign = '+' if pnl > 0 else ''
|
||||
|
||||
# 性价比
|
||||
cc = rec.get('cost_check', {})
|
||||
rr = cc.get('rr_ratio', rec.get('rr', 0))
|
||||
profit = cc.get('profit_amount', rec.get('tp_pnl', 0))
|
||||
fee = cc.get('fee_cost', 0)
|
||||
fee_pct = cc.get('fee_pct', 0)
|
||||
net = cc.get('net_profit', 0)
|
||||
rating_emoji = cc.get('rating_emoji', '⚠️')
|
||||
rating_text = cc.get('rating_text', '未知')
|
||||
|
||||
# 信号类型标签
|
||||
type_labels = {
|
||||
'open': '新开仓' if not fields.get('_is_add') else 'A类加仓',
|
||||
'reduce': 'B类减仓',
|
||||
'close': '平仓',
|
||||
}
|
||||
type_label = type_labels.get(signal_type, signal_type)
|
||||
|
||||
# 信号源仓位(只展示,不参与计算)
|
||||
src_info = f"📊 {trader} {size} {symbol}(价值${value})← 信号源,非你的仓位"
|
||||
|
||||
# 仓位变化对比
|
||||
try:
|
||||
current_size = float(fields.get('size', '0').replace(',', ''))
|
||||
comparison = format_comparison(trader, symbol, current_size)
|
||||
except:
|
||||
comparison = ""
|
||||
|
||||
# 交易员评分
|
||||
try:
|
||||
trader_rating = format_trader_rating(trader)
|
||||
except:
|
||||
trader_rating = ""
|
||||
|
||||
msg = f"""⚡ 跟单建议 | {symbol} {side_cn} {emoji} {leverage}x({type_label})
|
||||
|
||||
{src_info}
|
||||
入场: ${entry_price} | 当前: ${current}
|
||||
浮盈: {pnl_sign}{pnl:.0f} {pnl_emoji}
|
||||
|
||||
📊 仓位变化
|
||||
{comparison}
|
||||
|
||||
{trader_rating}
|
||||
|
||||
📐 性价比检查(基于你的推荐仓位)
|
||||
• 你的仓位: {rec['contracts']}张(保证金{rec['margin']:.2f} USDT)
|
||||
• 盈亏比: {rr}:1 {'✅' if rr >= 2 else '⚠️' if rr >= 1.5 else '❌'}
|
||||
• 盈利额: +{profit:.2f} USDT {'✅' if profit >= 10 else '❌ <10U保底'}
|
||||
• 手续费: {fee:.2f} USDT ({fee_pct:.1f}%) {'✅' if fee_pct < 5 else '❌'}
|
||||
• 净盈利: {net:.2f} USDT {'✅' if net >= 10 else '❌'}
|
||||
• 评级: {rating_emoji} {rating_text}
|
||||
• SL: ${rec['sl_price']}(-{rec['sl_pct']:.1f}%)
|
||||
• TP: ${rec['tp_price']}(+{rec['tp_pct']:.1f}%)
|
||||
|
||||
回复 Y 确认跟单 / N 取消"""
|
||||
|
||||
# 如果余额不足,替换跟单方案
|
||||
if rec.get('contracts', 0) == 0:
|
||||
msg = f"""⚡ 跟单建议 | {symbol} {side_cn} {emoji} {leverage}x({type_label})
|
||||
|
||||
{src_info}
|
||||
入场: ${entry_price} | 当前: ${current}
|
||||
浮盈: {pnl_sign}{pnl:.0f} {pnl_emoji}
|
||||
|
||||
⚠️ 余额不足,无法开仓
|
||||
• 可用: {rec.get('acct_free', 0):.2f} USDT
|
||||
• 需要: ~{rec.get('margin', 0):.2f} USDT
|
||||
|
||||
💡 建议:等待其他仓位止盈释放保证金"""
|
||||
|
||||
return msg
|
||||
|
||||
# ─── 推送 ────────────────────────────────────────────────────────────────
|
||||
|
||||
def push_to_qq(message):
|
||||
"""推送到QQ"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['bash', str(QQ_PUSH), message],
|
||||
capture_output=True, text=True, timeout=15
|
||||
)
|
||||
return result.returncode == 0
|
||||
except:
|
||||
return False
|
||||
|
||||
# ─── 执行订单 ────────────────────────────────────────────────────────────
|
||||
|
||||
def execute_order(symbol, side, leverage, rec):
|
||||
"""执行开仓订单"""
|
||||
cmd = [
|
||||
'python3', str(ADVISOR),
|
||||
'--symbol', symbol,
|
||||
'--side', side,
|
||||
'--leverage', str(leverage),
|
||||
'--execute', '--json',
|
||||
'--rec-json', json.dumps(rec)
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=30,
|
||||
cwd=str(ADVISOR.parent)
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return json.loads(result.stdout)
|
||||
else:
|
||||
return {'error': result.stderr.strip()[:200]}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
def format_execution_result(fields, rec, exec_result):
|
||||
"""格式化执行结果"""
|
||||
symbol = fields.get('symbol', '?')
|
||||
side_cn = fields.get('side', '做多')
|
||||
emoji = '🟩' if fields.get('side_en') == 'long' else '🟥'
|
||||
leverage = fields.get('leverage', '10')
|
||||
trader = fields.get('trader', '?')
|
||||
size = fields.get('size', '?')
|
||||
value = fields.get('value', '?')
|
||||
|
||||
cc = rec.get('cost_check', {})
|
||||
rr = cc.get('rr_ratio', rec.get('rr', 0))
|
||||
profit = cc.get('profit_amount', rec.get('tp_pnl', 0))
|
||||
fee = cc.get('fee_cost', 0)
|
||||
fee_pct = cc.get('fee_pct', 0)
|
||||
net = cc.get('net_profit', 0)
|
||||
rating_emoji = cc.get('rating_emoji', '⚠️')
|
||||
rating_text = cc.get('rating_text', '未知')
|
||||
|
||||
pos = exec_result.get('position', {})
|
||||
algo = exec_result.get('algo', {})
|
||||
|
||||
msg = f"""✅ {symbol} {side_cn} {emoji} {leverage}x 自动开仓
|
||||
|
||||
📊 信号源: {trader} {size} {symbol}(价值${value})
|
||||
|
||||
📐 性价比检查
|
||||
• 盈亏比: {rr}:1 ✅
|
||||
• 盈利额: +{profit:.2f} USDT ✅
|
||||
• 手续费: {fee:.2f} USDT ({fee_pct:.1f}%) ✅
|
||||
• 净盈利: {net:.2f} USDT ✅
|
||||
• 评级: {rating_emoji} {rating_text}
|
||||
|
||||
✅ 执行结果
|
||||
• 入场: ${pos.get('entry', rec.get('price', '?'))}
|
||||
• 仓位: {pos.get('contracts', rec.get('contracts', '?'))}张
|
||||
• TP: ${algo.get('tp', rec.get('tp_price', '?'))}
|
||||
• SL: ${algo.get('sl', rec.get('sl_price', '?'))}
|
||||
• 强平: ${pos.get('liq', '?')}
|
||||
|
||||
━━━ 当前全部持仓 ━━━
|
||||
(查询中...)"""
|
||||
|
||||
# 尝试获取当前全部持仓
|
||||
try:
|
||||
acct_cmd = ['python3', '-c', f'''
|
||||
import sys
|
||||
sys.path.insert(0, "{ADVISOR.parent}")
|
||||
from okx_position_advisor import load_credentials, create_exchange, get_account_info
|
||||
creds = load_credentials()
|
||||
exchange = create_exchange(creds)
|
||||
info = get_account_info(exchange)
|
||||
print(f"Free: {{info['usdt_free']:.2f}}")
|
||||
for p in info['positions']:
|
||||
print(f" {{p['symbol']}}: {{p['contracts']}}张 UPL={{p['pnl']:.2f}}")
|
||||
''']
|
||||
acct_result = subprocess.run(acct_cmd, capture_output=True, text=True, timeout=15)
|
||||
if acct_result.returncode == 0:
|
||||
msg = msg.replace("(查询中...)", f"\n```\n{acct_result.stdout.strip()}\n```")
|
||||
except:
|
||||
pass
|
||||
|
||||
return msg
|
||||
|
||||
# ─── 主流程 ──────────────────────────────────────────────────────────────
|
||||
|
||||
def process_signal(text):
|
||||
"""处理一条信号"""
|
||||
# 解析
|
||||
fields = parse_signal(text)
|
||||
fields['_raw'] = text
|
||||
|
||||
if not fields.get('symbol') or not fields.get('side'):
|
||||
return "⚠️ 无法解析信号"
|
||||
|
||||
symbol = fields['symbol']
|
||||
side = fields['side_en']
|
||||
leverage = fields.get('leverage', '10')
|
||||
trader = fields.get('trader', '未知')
|
||||
|
||||
# 去重
|
||||
dedup_conn = init_dedup_db()
|
||||
if is_duplicate(dedup_conn, text, symbol, trader):
|
||||
dedup_conn.close()
|
||||
return "⏭️ 重复信号,跳过"
|
||||
|
||||
# 分类
|
||||
signal_type = classify_signal(fields)
|
||||
|
||||
# 平仓信号直接推送
|
||||
if signal_type == 'close':
|
||||
msg = f"""🔔 {trader} {symbol}平仓提醒
|
||||
{text[text.find("入场"):text.find("回复")].strip() if "入场" in text else "详情见原始信号"}
|
||||
|
||||
💡 操作建议
|
||||
• 若已跟单{symbol},建议同步止盈/止损"""
|
||||
record_signal(dedup_conn, text, symbol, trader)
|
||||
dedup_conn.close()
|
||||
push_to_qq(msg)
|
||||
return "✅ 平仓信号已推送"
|
||||
|
||||
# 调advisor
|
||||
rec = run_advisor(symbol, side, leverage)
|
||||
|
||||
if 'error' in rec:
|
||||
record_signal(dedup_conn, text, symbol, trader)
|
||||
dedup_conn.close()
|
||||
return f"⚠️ advisor错误: {rec['error']}"
|
||||
|
||||
# 性价比检查
|
||||
cc = rec.get('cost_check', {})
|
||||
rr = cc.get('rr_ratio', rec.get('rr', 0))
|
||||
profit = cc.get('profit_amount', rec.get('tp_pnl', 0))
|
||||
fee_pct = cc.get('fee_pct', 0)
|
||||
auto_execute = cc.get('auto_execute', False) or (rr >= 2 and fee_pct < 5 and profit >= 10)
|
||||
|
||||
if auto_execute and signal_type == 'open':
|
||||
# 性价比高 + 新开仓 → 自动执行
|
||||
exec_result = execute_order(symbol, side, leverage, rec)
|
||||
if exec_result and 'error' not in exec_result:
|
||||
msg = format_execution_result(fields, rec, exec_result)
|
||||
_tracker_record(trader=trader, symbol=symbol, side=side,
|
||||
leverage=int(leverage) if leverage else 10,
|
||||
trader_size=float(fields.get('size', '0').replace(',', '')),
|
||||
trader_entry=float(fields.get('entry', '0').replace(',', '')),
|
||||
trader_pnl=float(fields.get('pnl', '0').replace(',', '')),
|
||||
raw_text=text, outcome='auto_executed')
|
||||
else:
|
||||
# 执行失败,降级为确认模式
|
||||
auto_execute = False
|
||||
msg = format_message(fields, rec, signal_type)
|
||||
_tracker_record(trader=trader, symbol=symbol, side=side,
|
||||
leverage=int(leverage) if leverage else 10,
|
||||
trader_size=float(fields.get('size', '0').replace(',', '')),
|
||||
trader_entry=float(fields.get('entry', '0').replace(',', '')),
|
||||
trader_pnl=float(fields.get('pnl', '0').replace(',', '')),
|
||||
raw_text=text, outcome='pushed')
|
||||
else:
|
||||
# 需要确认或减仓信号
|
||||
msg = format_message(fields, rec, signal_type)
|
||||
_tracker_record(trader=trader, symbol=symbol, side=side,
|
||||
leverage=int(leverage) if leverage else 10,
|
||||
trader_size=float(fields.get('size', '0').replace(',', '')),
|
||||
trader_entry=float(fields.get('entry', '0').replace(',', '')),
|
||||
trader_pnl=float(fields.get('pnl', '0').replace(',', '')),
|
||||
raw_text=text, outcome='pushed')
|
||||
|
||||
# 记录去重
|
||||
record_signal(dedup_conn, text, symbol, trader)
|
||||
dedup_conn.close()
|
||||
|
||||
# 推送
|
||||
success = push_to_qq(msg)
|
||||
if success:
|
||||
return f"✅ 已推送 | {symbol} {side} {leverage}x | {rec['contracts']}张 | 性价比{rec.get('cost_check', {}).get('rating_text', '?')}"
|
||||
else:
|
||||
return f"❌ 推送失败"
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1:
|
||||
text = ' '.join(sys.argv[1:])
|
||||
else:
|
||||
text = sys.stdin.read()
|
||||
|
||||
if not text.strip():
|
||||
print("用法: python3 process_signal.py '信号文本'")
|
||||
print("或: echo '信号文本' | python3 process_signal.py")
|
||||
return
|
||||
|
||||
result = process_signal(text)
|
||||
print(result)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user