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,468 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
信号历史数据库 - 记录所有交易信号
|
||||
用法:
|
||||
python3 signal_db.py log '<原始信号文本>'
|
||||
python3 signal_db.py history [--trader NAME] [--symbol BTC] [--days 7] [--limit 20]
|
||||
python3 signal_db.py stats
|
||||
python3 signal_db.py traders
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
DB_PATH = os.path.expanduser("~/.hermes/trading/signal_history.db")
|
||||
|
||||
def get_conn():
|
||||
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def init_db():
|
||||
conn = get_conn()
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS signals (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp REAL NOT NULL,
|
||||
time_str TEXT NOT NULL,
|
||||
trader TEXT,
|
||||
symbol TEXT,
|
||||
side TEXT,
|
||||
leverage INTEGER,
|
||||
raw_size REAL,
|
||||
raw_unit TEXT,
|
||||
entry_price REAL,
|
||||
current_price REAL,
|
||||
margin REAL,
|
||||
margin_unit TEXT,
|
||||
margin_mode TEXT,
|
||||
pnl REAL,
|
||||
pnl_pct REAL,
|
||||
leverage_change TEXT,
|
||||
raw_text TEXT NOT NULL,
|
||||
outcome TEXT DEFAULT 'pending',
|
||||
outcome_time REAL,
|
||||
outcome_detail TEXT
|
||||
)
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_signals_time ON signals(timestamp DESC)
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_signals_trader ON signals(trader)
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_signals_symbol ON signals(symbol)
|
||||
""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def extract_trader(text):
|
||||
"""Extract trader name from signal text.
|
||||
|
||||
Common patterns:
|
||||
- 【熬鹰资本】 (standalone 【name】 on its own line, no colon)
|
||||
- 【交易员】xxx
|
||||
- 【老师】xxx
|
||||
- 交易员: xxx
|
||||
- 来自xxx:
|
||||
- [xxx] at the beginning
|
||||
- @username
|
||||
- Name followed by colon (e.g. "张三: BTC做多")
|
||||
- Name followed by signal keywords
|
||||
"""
|
||||
# Priority 1: Standalone 【name】 on its own line (no colon after)
|
||||
# This matches 【熬鹰资本】 but NOT 【币种】: xxx
|
||||
standalone = re.search(r'^【([^】]{1,20})】\s*$', text, re.MULTILINE)
|
||||
if standalone:
|
||||
return standalone.group(1).strip()
|
||||
|
||||
patterns = [
|
||||
r'【交易员】\s*(.+?)(?:\n|$|【)',
|
||||
r'【老师】\s*(.+?)(?:\n|$|【)',
|
||||
r'【来源】\s*(.+?)(?:\n|$|【)',
|
||||
r'【策略】\s*(.+?)(?:\n|$|【)',
|
||||
r'交易员[::]\s*(.+?)(?:\n|$)',
|
||||
r'老师[::]\s*(.+?)(?:\n|$)',
|
||||
r'来源[::]\s*(.+?)(?:\n|$)',
|
||||
r'策略师[::]\s*(.+?)(?:\n|$)',
|
||||
r'^\[([^\]]+)\]', # [TraderName] at start
|
||||
r'^(@\w+)', # @username at start
|
||||
r'^(\S+?)\s*[::]\s*(?:【|BTC|ETH|做多|做空|开多|开空)', # Name: signal
|
||||
r'^(\S{2,10})\s+(?:【|BTC|ETH|做多|做空|开多|开空)', # Name signal (no colon)
|
||||
]
|
||||
for p in patterns:
|
||||
m = re.search(p, text, re.MULTILINE)
|
||||
if m:
|
||||
name = m.group(1).strip()
|
||||
# Filter out non-name matches
|
||||
if len(name) > 1 and len(name) < 30 and not re.match(r'^[\d.]+$', name):
|
||||
return name
|
||||
return None
|
||||
|
||||
def extract_signal_fields(text):
|
||||
"""Parse signal text for key fields."""
|
||||
result = {'trader': extract_trader(text)}
|
||||
|
||||
# Symbol - multiple patterns
|
||||
m = re.search(r'(?:【币种】|币种[::]\s*)(\w+)', text)
|
||||
if not m:
|
||||
m = re.search(r'([A-Z]{2,10})USDT', text)
|
||||
if not m:
|
||||
# Bare symbol before direction keywords (e.g. "ETH做空", "BTC 开多")
|
||||
m = re.search(r'\b([A-Z]{2,10})\s*(?:做多|做空|开多|开空|做多|做空|long|short)', text, re.IGNORECASE)
|
||||
if m:
|
||||
raw = m.group(1).upper().replace("USDT", "").replace("/USDT", "").replace(":USDT", "")
|
||||
if len(raw) >= 2:
|
||||
result['symbol'] = raw
|
||||
|
||||
# Side
|
||||
if re.search(r'(做空|卖出|short|sell|空单|开空)', text, re.IGNORECASE):
|
||||
result['side'] = 'short'
|
||||
elif re.search(r'(做多|买入|long|buy|多单|开多)', text, re.IGNORECASE):
|
||||
result['side'] = 'long'
|
||||
|
||||
# Leverage from field
|
||||
m = re.search(r'(?:【币种】|币种[::]\s*)[^\n]*?(\d+)\s*[xX倍]', text)
|
||||
if not m:
|
||||
m = re.search(r'(\d+)\s*[xX倍]', text)
|
||||
result['leverage'] = int(m.group(1)) if m else None
|
||||
|
||||
# Size
|
||||
m = re.search(r'(?:【仓位】|仓位[::]\s*)([\d,.]+)\s*(\w+)', text)
|
||||
if m:
|
||||
result['raw_size'] = float(m.group(1).replace(",", ""))
|
||||
result['raw_unit'] = m.group(2)
|
||||
|
||||
# Entry price
|
||||
m = re.search(r'【开仓价】\s*[::]?\s*([\d,.]+)', text)
|
||||
if m:
|
||||
result['entry_price'] = float(m.group(1).replace(",", ""))
|
||||
|
||||
# Current price
|
||||
m = re.search(r'【当前价】\s*[::]?\s*([\d,.]+)', text)
|
||||
if m:
|
||||
result['current_price'] = float(m.group(1).replace(",", ""))
|
||||
|
||||
# Margin
|
||||
m = re.search(r'【保证金】\s*[::]?\s*([\d,.]+)\s*(\w+)', text)
|
||||
if m:
|
||||
result['margin'] = float(m.group(1).replace(",", ""))
|
||||
result['margin_unit'] = m.group(2)
|
||||
|
||||
# Margin mode (全仓/逐仓)
|
||||
m = re.search(r'(全仓|逐仓)', text)
|
||||
if m:
|
||||
result['margin_mode'] = m.group(1)
|
||||
|
||||
# PnL
|
||||
m = re.search(r'【收益额】\s*[::]?\s*([-\d,.]+)\s*(\w+)', text)
|
||||
if m:
|
||||
result['pnl'] = float(m.group(1).replace(",", ""))
|
||||
m = re.search(r'【收益额】\s*[::]?\s*[-\d,.]+\s*\w+\(([-\d.]+)%\)', text)
|
||||
if m:
|
||||
result['pnl_pct'] = float(m.group(1))
|
||||
|
||||
# Leverage change (e.g. "5→10")
|
||||
m = re.search(r'修改了杠杆\s*(\d+)\s*[→>→]\s*(\d+)', text)
|
||||
if m:
|
||||
result['leverage_change'] = f"{m.group(1)}→{m.group(2)}"
|
||||
|
||||
# Is close signal
|
||||
result['is_close'] = bool(re.search(r'(平仓|止盈|止损|close|全平)', text, re.IGNORECASE))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def log_signal(raw_text):
|
||||
"""Log a signal to the database."""
|
||||
init_db()
|
||||
fields = extract_signal_fields(raw_text)
|
||||
|
||||
conn = get_conn()
|
||||
now = time.time()
|
||||
time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
conn.execute("""
|
||||
INSERT INTO signals (timestamp, time_str, trader, symbol, side, leverage,
|
||||
raw_size, raw_unit, entry_price, current_price,
|
||||
margin, margin_unit, margin_mode, pnl, pnl_pct,
|
||||
leverage_change, raw_text)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
now, time_str,
|
||||
fields.get('trader'),
|
||||
fields.get('symbol'),
|
||||
fields.get('side'),
|
||||
fields.get('leverage'),
|
||||
fields.get('raw_size'),
|
||||
fields.get('raw_unit'),
|
||||
fields.get('entry_price'),
|
||||
fields.get('current_price'),
|
||||
fields.get('margin'),
|
||||
fields.get('margin_unit'),
|
||||
fields.get('margin_mode'),
|
||||
fields.get('pnl'),
|
||||
fields.get('pnl_pct'),
|
||||
fields.get('leverage_change'),
|
||||
raw_text,
|
||||
))
|
||||
signal_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
'id': signal_id,
|
||||
'time': time_str,
|
||||
'trader': fields.get('trader'),
|
||||
'symbol': fields.get('symbol'),
|
||||
'side': fields.get('side'),
|
||||
'leverage': fields.get('leverage'),
|
||||
}
|
||||
|
||||
def update_outcome(signal_id, outcome, detail=""):
|
||||
"""Update signal outcome (confirmed/cancelled/expired)."""
|
||||
conn = get_conn()
|
||||
conn.execute("""
|
||||
UPDATE signals SET outcome=?, outcome_time=?, outcome_detail=?
|
||||
WHERE id=?
|
||||
""", (outcome, time.time(), detail, signal_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def find_latest_signal_id(symbol):
|
||||
"""Find the most recent pending signal ID for a symbol."""
|
||||
conn = get_conn()
|
||||
row = conn.execute("""
|
||||
SELECT id FROM signals WHERE symbol=? AND outcome='pending'
|
||||
ORDER BY timestamp DESC LIMIT 1
|
||||
""", (symbol,)).fetchone()
|
||||
conn.close()
|
||||
return row['id'] if row else None
|
||||
|
||||
def query_history(trader=None, symbol=None, days=7, limit=20):
|
||||
"""Query signal history with filters."""
|
||||
init_db()
|
||||
conn = get_conn()
|
||||
|
||||
conditions = ["timestamp > ?"]
|
||||
params = [time.time() - days * 86400]
|
||||
|
||||
if trader:
|
||||
conditions.append("trader LIKE ?")
|
||||
params.append(f"%{trader}%")
|
||||
if symbol:
|
||||
conditions.append("symbol LIKE ?")
|
||||
params.append(f"%{symbol}%")
|
||||
|
||||
where = " AND ".join(conditions)
|
||||
rows = conn.execute(f"""
|
||||
SELECT * FROM signals WHERE {where}
|
||||
ORDER BY timestamp DESC LIMIT ?
|
||||
""", params + [limit]).fetchall()
|
||||
conn.close()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
def get_trader_stats():
|
||||
"""Get stats per trader."""
|
||||
init_db()
|
||||
conn = get_conn()
|
||||
rows = conn.execute("""
|
||||
SELECT
|
||||
trader,
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN outcome='confirmed' THEN 1 ELSE 0 END) as confirmed,
|
||||
SUM(CASE WHEN outcome='cancelled' THEN 1 ELSE 0 END) as cancelled,
|
||||
SUM(CASE WHEN outcome='pending' THEN 1 ELSE 0 END) as pending,
|
||||
SUM(CASE WHEN outcome='expired' THEN 1 ELSE 0 END) as expired,
|
||||
GROUP_CONCAT(DISTINCT symbol) as symbols
|
||||
FROM signals
|
||||
GROUP BY trader
|
||||
ORDER BY total DESC
|
||||
""").fetchall()
|
||||
conn.close()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
def get_summary_stats():
|
||||
"""Get overall summary stats."""
|
||||
init_db()
|
||||
conn = get_conn()
|
||||
|
||||
total = conn.execute("SELECT COUNT(*) as c FROM signals").fetchone()['c']
|
||||
today = conn.execute(
|
||||
"SELECT COUNT(*) as c FROM signals WHERE timestamp > ?",
|
||||
(time.time() - 86400,)
|
||||
).fetchone()['c']
|
||||
|
||||
by_outcome = conn.execute("""
|
||||
SELECT outcome, COUNT(*) as c FROM signals GROUP BY outcome
|
||||
""").fetchall()
|
||||
|
||||
by_side = conn.execute("""
|
||||
SELECT side, COUNT(*) as c FROM signals WHERE side IS NOT NULL GROUP BY side
|
||||
""").fetchall()
|
||||
|
||||
top_symbols = conn.execute("""
|
||||
SELECT symbol, COUNT(*) as c FROM signals
|
||||
WHERE symbol IS NOT NULL
|
||||
GROUP BY symbol ORDER BY c DESC LIMIT 5
|
||||
""").fetchall()
|
||||
|
||||
conn.close()
|
||||
return {
|
||||
'total': total,
|
||||
'today': today,
|
||||
'by_outcome': {r['outcome']: r['c'] for r in by_outcome},
|
||||
'by_side': {r['side']: r['c'] for r in by_side},
|
||||
'top_symbols': [(r['symbol'], r['c']) for r in top_symbols],
|
||||
}
|
||||
|
||||
|
||||
def format_history(signals):
|
||||
"""Format history for display."""
|
||||
if not signals:
|
||||
return "📭 暂无信号记录"
|
||||
|
||||
lines = ["📋 **信号历史记录**\n"]
|
||||
for s in signals:
|
||||
side_cn = "做多" if s['side'] == 'long' else ("做空" if s['side'] == 'short' else "?")
|
||||
outcome_emoji = {
|
||||
'confirmed': '✅', 'cancelled': '❌', 'pending': '⏳', 'expired': '⏰'
|
||||
}.get(s['outcome'], '❓')
|
||||
trader = s['trader'] or '未知'
|
||||
lev = f"{s['leverage']}x" if s['leverage'] else '?x'
|
||||
|
||||
# Extra info
|
||||
extra = []
|
||||
if s.get('entry_price'):
|
||||
extra.append(f"入场{s['entry_price']}")
|
||||
if s.get('pnl'):
|
||||
pnl_str = f"{s['pnl']:+,.0f}"
|
||||
if s.get('pnl_pct'):
|
||||
pnl_str += f"({s['pnl_pct']:+.1f}%)"
|
||||
extra.append(f"盈亏{pnl_str}")
|
||||
if s.get('margin'):
|
||||
extra.append(f"保证金{s['margin']:,.0f}")
|
||||
if s.get('margin_mode'):
|
||||
extra.append(s['margin_mode'])
|
||||
if s.get('leverage_change'):
|
||||
extra.append(f"杠杆{s['leverage_change']}")
|
||||
|
||||
extra_str = " | " + " ".join(extra) if extra else ""
|
||||
|
||||
lines.append(
|
||||
f"{outcome_emoji} #{s['id']} | {s['time_str']} | "
|
||||
f"👤{trader} | {s['symbol'] or '?'} {side_cn} | "
|
||||
f"{lev}{extra_str}"
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def format_stats(stats):
|
||||
"""Format stats for display."""
|
||||
lines = ["📊 **信号统计**\n"]
|
||||
lines.append(f"总计: {stats['total']} 条")
|
||||
lines.append(f"今日: {stats['today']} 条\n")
|
||||
|
||||
if stats['by_outcome']:
|
||||
lines.append("**按结果:**")
|
||||
for k, v in stats['by_outcome'].items():
|
||||
emoji = {'confirmed': '✅', 'cancelled': '❌', 'pending': '⏳', 'expired': '⏰'}.get(k, '❓')
|
||||
lines.append(f" {emoji} {k}: {v}")
|
||||
|
||||
if stats['by_side']:
|
||||
lines.append("\n**按方向:**")
|
||||
for k, v in stats['by_side'].items():
|
||||
cn = "做多" if k == 'long' else "做空"
|
||||
lines.append(f" {cn}: {v}")
|
||||
|
||||
if stats['top_symbols']:
|
||||
lines.append("\n**热门币种:**")
|
||||
for sym, cnt in stats['top_symbols']:
|
||||
lines.append(f" {sym}: {cnt}次")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def format_traders(traders):
|
||||
"""Format trader stats for display."""
|
||||
if not traders:
|
||||
return "📭 暂无交易员数据"
|
||||
|
||||
lines = ["👤 **交易员统计**\n"]
|
||||
for t in traders:
|
||||
name = t['trader'] or '未知'
|
||||
lines.append(
|
||||
f"**{name}**: {t['total']}条信号 | "
|
||||
f"✅{t['confirmed']} ❌{t['cancelled']} ⏳{t['pending']} | "
|
||||
f"币种: {t['symbols'] or '-'}"
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("用法: signal_db.py <log|history|stats|traders|update> [args]")
|
||||
sys.exit(1)
|
||||
|
||||
action = sys.argv[1]
|
||||
|
||||
if action == "log":
|
||||
if len(sys.argv) < 3:
|
||||
print("用法: signal_db.py log '<raw_text>'")
|
||||
sys.exit(1)
|
||||
raw_text = sys.argv[2]
|
||||
result = log_signal(raw_text)
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
|
||||
elif action == "history":
|
||||
import argparse
|
||||
# Simple arg parsing
|
||||
trader = symbol = None
|
||||
days = 7
|
||||
limit = 20
|
||||
for i in range(2, len(sys.argv)):
|
||||
if sys.argv[i] == "--trader" and i + 1 < len(sys.argv):
|
||||
trader = sys.argv[i + 1]
|
||||
elif sys.argv[i] == "--symbol" and i + 1 < len(sys.argv):
|
||||
symbol = sys.argv[i + 1]
|
||||
elif sys.argv[i] == "--days" and i + 1 < len(sys.argv):
|
||||
days = int(sys.argv[i + 1])
|
||||
elif sys.argv[i] == "--limit" and i + 1 < len(sys.argv):
|
||||
limit = int(sys.argv[i + 1])
|
||||
signals = query_history(trader, symbol, days, limit)
|
||||
print(format_history(signals))
|
||||
|
||||
elif action == "stats":
|
||||
stats = get_summary_stats()
|
||||
print(format_stats(stats))
|
||||
|
||||
elif action == "traders":
|
||||
traders = get_trader_stats()
|
||||
print(format_traders(traders))
|
||||
|
||||
elif action == "update":
|
||||
if len(sys.argv) < 4:
|
||||
print("用法: signal_db.py update <signal_id> <outcome> [detail]")
|
||||
sys.exit(1)
|
||||
signal_id = int(sys.argv[2])
|
||||
outcome = sys.argv[3]
|
||||
detail = sys.argv[4] if len(sys.argv) > 4 else ""
|
||||
update_outcome(signal_id, outcome, detail)
|
||||
print(f"✅ Updated signal #{signal_id} → {outcome}")
|
||||
|
||||
else:
|
||||
print(f"Unknown action: {action}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user