261 lines
10 KiB
Python
261 lines
10 KiB
Python
"""
|
||
山寨币状态跟踪器 — 去重 + 状态升级管理
|
||
|
||
状态生命周期:蓄力 → 加速 → 爁发 → 已告警 → 过期
|
||
只有状态升级才告警,同级别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)}个") |