alphax/coin_state_tracker.py
2026-05-13 22:32:50 +08:00

261 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
山寨币状态跟踪器 — 去重 + 状态升级管理
状态生命周期:蓄力 → 加速 → 爁发 → 已告警 → 过期
只有状态升级才告警同级别12h内不重复推送
"""
import sqlite3
import json
from datetime import datetime, timedelta
DB_PATH = "/home/ubuntu/quant_monitor/altcoin/altcoin_monitor.db"
STATE_ORDER = {
"蓄力": 1,
"加速": 2,
"爆发": 3,
"已告警": 4,
"过期": 5,
}
ALERT_LEVELS = {
"蓄力": "low",
"加速": "medium",
"爆发": "high",
}
# 12h内同级别不重复告警
ALERT_COOLDOWN_HOURS = 12
# 24h后状态自动过期
EXPIRE_HOURS = 24
def init_db():
"""初始化数据库"""
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS coin_state (
symbol TEXT PRIMARY KEY,
state TEXT NOT NULL,
score REAL DEFAULT 0,
anomaly_type TEXT DEFAULT '',
sector TEXT DEFAULT '',
leader_status TEXT DEFAULT '',
detected_at TEXT NOT NULL,
last_alert_time TEXT DEFAULT '',
last_alert_level TEXT DEFAULT '',
detail_json TEXT DEFAULT '{}'
)
""")
conn.commit()
conn.close()
def get_state(symbol):
"""获取币种当前状态"""
conn = sqlite3.connect(DB_PATH)
row = conn.execute("SELECT * FROM coin_state WHERE symbol=?", (symbol,)).fetchone()
conn.close()
if row:
return {
"symbol": row[0],
"state": row[1],
"score": row[2],
"anomaly_type": row[3],
"sector": row[4],
"leader_status": row[5],
"detected_at": row[6],
"last_alert_time": row[7],
"last_alert_level": row[8],
"detail": json.loads(row[9]) if row[9] else {},
}
return None
def update_state(symbol, new_state, score=0, anomaly_type="", sector="", leader_status="", detail={}):
"""
更新币种状态,判断是否需要告警
返回: {"should_alert": bool, "alert_level": str, "reason": str}
"""
current = get_state(symbol)
now = datetime.now().isoformat()
should_alert = False
alert_level = ""
reason = ""
if current:
current_level = STATE_ORDER.get(current["state"], 0)
new_level = STATE_ORDER.get(new_state, 0)
# 状态升级 → 检查冷却时间
if new_level > current_level:
last_alert_time = current.get("last_alert_time", "") or ""
last_alert_level_str = current.get("last_alert_level", "") or ""
if not last_alert_time:
# 没有上次告警记录 → 状态升级肯定要告警
should_alert = True
alert_level = ALERT_LEVELS.get(new_state, "low")
reason = f"状态升级(首次告警): {current['state']}{new_state}"
else:
last_dt = datetime.fromisoformat(last_alert_time)
cooldown_end = last_dt + timedelta(hours=ALERT_COOLDOWN_HOURS)
# 新级别比上次告警级别高 → 不管冷却期都要告警
last_alert_num = STATE_ORDER.get(last_alert_level_str, 0)
if new_level > last_alert_num:
should_alert = True
alert_level = ALERT_LEVELS.get(new_state, "low")
reason = f"状态升级: {current['state']}{new_state}"
elif now > cooldown_end.isoformat():
# 冷却期过了,可以再告警
should_alert = True
alert_level = ALERT_LEVELS.get(new_state, "low")
reason = f"冷却期结束,重新告警: {new_state}"
else:
should_alert = False
reason = f"冷却期内(上次告警: {last_alert_level_str} @ {last_alert_time})"
# 状态降级 → 标记为"信号消退"
elif new_level < current_level:
should_alert = False
reason = f"信号消退: {current['state']}{new_state}"
# 同级别,分数显著提升(≥3分) → 也告警
elif new_level == current_level and score - current["score"] >= 3:
last_alert_time = current.get("last_alert_time", "") or ""
if not last_alert_time:
should_alert = True
alert_level = ALERT_LEVELS.get(new_state, "low")
reason = f"同级别分数显著提升(首次): {current['score']}{score}"
else:
last_dt = datetime.fromisoformat(last_alert_time)
cooldown_end = last_dt + timedelta(hours=ALERT_COOLDOWN_HOURS)
if now > cooldown_end.isoformat():
should_alert = True
alert_level = ALERT_LEVELS.get(new_state, "low")
reason = f"同级别但分数显著提升: {current['score']}{score}"
# 更新记录
conn = sqlite3.connect(DB_PATH)
alert_time = now if should_alert else current.get("last_alert_time", "")
alert_lvl = alert_level if should_alert else current.get("last_alert_level", "")
conn.execute("""
UPDATE coin_state SET state=?, score=?, anomaly_type=?, sector=?,
leader_status=?, detail_json=?, last_alert_time=?, last_alert_level=?
WHERE symbol=?
""", (new_state, score, anomaly_type, sector, leader_status,
json.dumps(detail, ensure_ascii=False), alert_time, alert_lvl, symbol))
conn.commit()
conn.close()
else:
# 新币种,首次检测
# 蓄力级别首次不告警(太多噪音),加速和爆发才告警
if STATE_ORDER.get(new_state, 0) >= STATE_ORDER.get("加速", 0):
should_alert = True
alert_level = ALERT_LEVELS.get(new_state, "low")
reason = f"新检测: {new_state}"
conn = sqlite3.connect(DB_PATH)
alert_time = now if should_alert else ""
alert_lvl = alert_level if should_alert else ""
conn.execute("""
INSERT INTO coin_state (symbol, state, score, anomaly_type, sector,
leader_status, detected_at, last_alert_time, last_alert_level, detail_json)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (symbol, new_state, score, anomaly_type, sector, leader_status,
now, alert_time, alert_lvl, json.dumps(detail, ensure_ascii=False)))
conn.commit()
conn.close()
return {"should_alert": should_alert, "alert_level": alert_level, "reason": reason}
def get_all_active():
"""获取所有活跃状态(未过期的)"""
conn = sqlite3.connect(DB_PATH)
cutoff = (datetime.now() - timedelta(hours=EXPIRE_HOURS)).isoformat()
rows = conn.execute("""
SELECT symbol, state, score, anomaly_type, sector, leader_status, detected_at
FROM coin_state WHERE detected_at > ? AND state != '过期'
ORDER BY score DESC
""", (cutoff,)).fetchall()
conn.close()
return [{
"symbol": r[0], "state": r[1], "score": r[2],
"anomaly_type": r[3], "sector": r[4], "leader_status": r[5],
"detected_at": r[6]
} for r in rows]
def get_candidates_for_confirm():
"""获取加速状态的候选(需要第三层确认的)"""
conn = sqlite3.connect(DB_PATH)
cutoff = (datetime.now() - timedelta(hours=EXPIRE_HOURS)).isoformat()
rows = conn.execute("""
SELECT symbol, state, score, anomaly_type, sector, leader_status, detail_json
FROM coin_state WHERE state IN ('加速', '蓄力') AND detected_at > ?
AND score >= 6
ORDER BY score DESC
""", (cutoff,)).fetchall()
conn.close()
return [{
"symbol": r[0], "state": r[1], "score": r[2],
"anomaly_type": r[3], "sector": r[4], "leader_status": r[5],
"detail": json.loads(r[6]) if r[6] else {},
} for r in rows]
def expire_old_states():
"""过期超过24h的状态"""
conn = sqlite3.connect(DB_PATH)
cutoff = (datetime.now() - timedelta(hours=EXPIRE_HOURS)).isoformat()
conn.execute("UPDATE coin_state SET state='过期' WHERE detected_at < ? AND state != '过期'", (cutoff,))
conn.commit()
conn.close()
if __name__ == "__main__":
init_db()
print("DB初始化完成")
# 测试状态升级
r1 = update_state("FET/USDT", "蓄力", score=3, anomaly_type="布林收窄+量突变", sector="AI_DePIN")
print(f"FET 蓄力: alert={r1['should_alert']}, reason={r1['reason']}")
r2 = update_state("FET/USDT", "加速", score=8, anomaly_type="MACD金叉+RSI拐点", sector="AI_DePIN")
print(f"FET 加速: alert={r2['should_alert']}, reason={r2['reason']}")
r3 = update_state("FET/USDT", "爆发", score=12, anomaly_type="1H放量突破", sector="AI_DePIN")
print(f"FET 爁发: alert={r3['should_alert']}, reason={r3['reason']}")
# 测试同级别不重复
r4 = update_state("FET/USDT", "爆发", score=13, anomaly_type="1H放量突破+均线多头", sector="AI_DePIN")
print(f"FET 爁发(重复): alert={r4['should_alert']}, reason={r4['reason']}")
# 测试分数显著提升
r5 = update_state("FET/USDT", "爆发", score=16, anomaly_type="三级共振", sector="AI_DePIN")
print(f"FET 爁发(分数升3+): alert={r5['should_alert']}, reason={r5['reason']}")
# 测试新币种首次检测
r6 = update_state("PEPE/USDT", "蓄力", score=3, sector="MEME")
print(f"PEPE 蓄力(首次): alert={r6['should_alert']}, reason={r6['reason']}")
r7 = update_state("PEPE/USDT", "加速", score=8, sector="MEME")
print(f"PEPE 加速(首次): alert={r7['should_alert']}, reason={r7['reason']}")
# 查看活跃状态
active = get_all_active()
print(f"\n活跃状态: {len(active)}")
for a in active:
print(f" {a['symbol']}: {a['state']} (score={a['score']})")
# 查看需要确认的候选
candidates = get_candidates_for_confirm()
print(f"\n需要确认的候选: {len(candidates)}")