- 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
469 lines
15 KiB
Python
469 lines
15 KiB
Python
#!/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()
|