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

434 lines
19 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.

"""
山寨币爆发监控系统 v11.7.9 — 价格跟踪+三级分级跟踪止盈+趋势反转(纯前瞻版)
趋势反转检测1H连续阴动K、量价背离、空头加速替代MACD/RSI
v1.7.9: 跟踪止盈按盈利分三级 — 防震(3×ATR) → 锁利(2×ATR) → 紧贴(1.2×ATR)
止盈止损跟踪、动态买入/卖出指引
"""
import sys, os, shutil
# ⚠️ 安全机制启动时强制清__pycache__防止旧版字节码残留
for cache_dir in [
os.path.join(os.path.dirname(__file__), "__pycache__"),
os.path.join(os.path.dirname(__file__), "..", "__pycache__"),
]:
if os.path.exists(cache_dir):
shutil.rmtree(cache_dir, ignore_errors=True)
import ccxt
import pandas as pd
import json
import sys
import os
from datetime import datetime
sys.path.insert(0, os.path.dirname(__file__))
from altcoin_db import (
init_db, get_active_recommendations, update_recommendation_tracking,
expire_old_recommendations, get_stats, update_recommendation_action_status,
should_push, log_push, apply_recommendation_state_transition, log_cron_run,
update_latest_price_cache,
)
from pa_engine import (
calc_atr, full_pa_analysis, detect_trend_exhaustion,
analyze_entry_point,
)
from feishu_push import push_altcoin_tp_sl_alert
from config_loader import load_rules
from opportunity_lifecycle import apply_entry_quality_gate
exchange = ccxt.binance({"enableRateLimit": True})
def fetch_klines(symbol, timeframe, limit=200):
try:
ohlcv = exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
df = pd.DataFrame(ohlcv, columns=["timestamp", "open", "high", "low", "close", "volume"])
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms")
return df
except Exception:
return None
def analyze_tracking_signals(symbol, rec, current_price):
"""
对active推荐做动态跟踪分析
返回: {"action_status": str, "sell_signals": [...], "buy_signals": [...],
"exhaustion": {...}, "entry_update": {...}}
"""
sell_signals = []
buy_signals = []
action_status = "持有" # 默认状态
entry_price = rec["entry_price"]
stop_loss = rec.get("stop_loss", entry_price * 0.95)
tp1 = rec.get("tp1", entry_price * 1.03)
tp2 = rec.get("tp2", entry_price * 1.05)
entry_plan = rec.get("entry_plan") or {}
# ---- 拉取1H数据做趋势分析 ----
h1_df = fetch_klines(symbol, "1h", limit=100)
m15_df = fetch_klines(symbol, "15m", limit=100)
h4_df = fetch_klines(symbol, "4h", limit=100)
atr_1h = calc_atr(h1_df, 14) if h1_df is not None else 0
# ---- 趋势衰减检测 ----
exhaustion = {}
if h1_df is not None and atr_1h > 0:
exhaustion = detect_trend_exhaustion(h1_df, atr_1h)
if exhaustion.get("exhausted"):
for es in exhaustion.get("signals", []):
sell_signals.append(es)
if exhaustion["severity"] == "high":
action_status = "衰减"
elif exhaustion["severity"] == "medium":
# 中度衰减,还持有但需关注
sell_signals.append("趋势中度衰减,关注止盈")
# ---- 止盈信号检测 ----
pnl_pct = ((current_price / entry_price) - 1) * 100 if entry_price > 0 else 0
# 到达TP1v1.7.8TP1保留作为提醒目标
if tp1 > 0 and current_price >= tp1:
sell_signals.append(f"✅ 到达TP1(${tp1:.4f}), 建议止盈50%仓位")
action_status = "止盈1" # 无条件TP1到了就是止盈无论之前什么状态
# === v1.7.8 跟踪止盈全面升级 ===
# 核心改动:
# ① 激活门槛 5%→3% (抓更多利润)
# ② ATR乘数 2.0→1.5 (更紧贴行情)
# ③ 每次tracker运行都重新计算并只升不降 (动态跟随行情逐步抬高)
# ④ 跟踪止盈触发时无条件覆盖所有状态 (利润保护优先级最高)
# ⑤ TP2已废除(历史0命中),跟踪止盈是唯一动态止盈方式
rules = load_rules()
trail_cfg = rules.get("tracker", {}).get("trailing_stop", {})
trailing_stop_level = entry_plan.get("trailing_stop_level", 0)
if trail_cfg.get("enabled", True) and atr_1h > 0 and entry_price > 0:
activate_pct = trail_cfg.get("activate_pnl_pct", 3)
# === v1.7.9 三级分级乘数: 按盈利切换 ===
tiers = trail_cfg.get("tiers", [])
trail_atr_mult = 1.5 # 兜底
tier_label = ""
if tiers:
for t in sorted(tiers, key=lambda x: x.get("min_pnl_pct", 0), reverse=True):
if pnl_pct >= t.get("min_pnl_pct", 0):
trail_atr_mult = t.get("atr_mult", 1.5)
tier_label = t.get("label", "")
break
# === 动态跟随:每次运行都重新计算跟踪止盈位 ===
# 算法: trailing_stop = current_price - atr_mult × ATR_1h
# 规则: 只升不降 (max(old_level, new_level))
# 激活条件: pnl_pct ≥ activate_pct (3%)
if pnl_pct >= activate_pct:
new_trail = current_price - trail_atr_mult * atr_1h
if trailing_stop_level > 0:
# 已有跟踪位 → 只上移不下移
trailing_stop_level = max(trailing_stop_level, new_trail)
else:
# 首次激活
trailing_stop_level = new_trail
tier_info = f" [{tier_label}·{trail_atr_mult}×ATR]" if tier_label else ""
sell_signals.append(f"🎯 跟踪止盈激活(盈+{pnl_pct:.1f}%≥{activate_pct}%{tier_info}, 回撤{trail_atr_mult}×ATR触发)")
# === 触发检查:当前价跌破跟踪止盈位 → 止盈 ===
# 🔴 v1.7.8: 跟踪止盈触发时无条件覆盖(利润保护优先级最高)
if trailing_stop_level > 0 and current_price <= trailing_stop_level:
drop_from_trail = trailing_stop_level - current_price
sell_signals.append(f"🎯 跟踪止盈触发! 从高位回撤${drop_from_trail:.4f}({drop_from_trail/trailing_stop_level*100:.1f}%)")
action_status = "跟踪止盈"
# 定期报告(即使没触发也显示跟踪位)
if trailing_stop_level > 0 and current_price > trailing_stop_level and pnl_pct >= activate_pct * 2:
cushion = (current_price - trailing_stop_level) / current_price * 100
sell_signals.append(f"📊 跟踪止盈中: 止盈位${trailing_stop_level:.4f}(距现价{cushion:.1f}%)")
elif pnl_pct >= 1 and trailing_stop_level > 0 and current_price <= trailing_stop_level:
# 利润回落到1%以内但跟踪位已激活且被击穿 → 保本出
sell_signals.append(f"🔒 保本止盈: 利润缩至{pnl_pct:.1f}%,跟踪位击穿")
action_status = "跟踪止盈"
# ---- 无TP保护的推荐涨超15%自动止盈(孤儿推荐保护)----
# 加速推荐(粗筛/细筛层)没有 TP/SLprice_tracker 无法判断出场点。
# 涨超15%仍无结论 → 认怂落袋,避免永续浮盈不兑现。
if tp1 == 0 and pnl_pct >= 15:
sell_signals.append(f"✅ 无TP保护自动止盈(涨+{pnl_pct:.1f}%≥15%,落袋为安)")
action_status = "止盈1"
# ---- 止损接近警告 ----
if stop_loss > 0:
loss_pct = ((current_price / stop_loss) - 1) * 100
if loss_pct < 3: # 当前价离止损不到3%
sell_signals.append(f"⚠️ 接近止损!当前${current_price:.4f}离止损${stop_loss:.4f}{loss_pct:.1f}%")
if current_price <= stop_loss:
sell_signals.append(f"🔴 已触发止损!${current_price:.4f}≤${stop_loss:.4f}")
action_status = "止损"
# ---- 趋势反转信号PA行为检测替代MACD ----
if h1_df is not None and len(h1_df) >= 30 and atr_1h > 0:
pa_1h = full_pa_analysis(h1_df, "1h")
pa_1h_candles = pa_1h.get("candles_class", [])
recent_candles = pa_1h_candles[-6:] if len(pa_1h_candles) >= 6 else pa_1h_candles
# 1H连续阴动K → 趋势反转
dy_bear_count = sum(1 for c in recent_candles if c["type"] == "dynamic" and c["direction"] == -1)
if dy_bear_count >= 3:
sell_signals.append(f"🔴 1H连续{dy_bear_count}根阴动K(趋势反转)")
if action_status == "持有":
action_status = "反转"
# 1H量价背离放量但阴线多头出货
avg_vol = float(h1_df["volume"].rolling(20).mean().iloc[-1])
recent_3 = h1_df.tail(3)
high_vol_bear = 0
for _, row in recent_3.iterrows():
vol_r = row["volume"] / avg_vol if avg_vol > 0 else 0
if vol_r >= 2 and row["close"] < row["open"]:
high_vol_bear += 1
if high_vol_bear >= 2:
sell_signals.append("🔴 1H放量阴线×2(多头出货)")
# 1H连续K空头加速
cont_k = pa_1h.get("continuous_k", [])
for ck in cont_k:
if ck["type"] == "bearish_continue" and ck["length"] >= 3:
sell_signals.append(f"🔴 1H连续{ck['length']}K空头加速")
if action_status == "持有":
action_status = "反转"
# === 时间衰减 (v1.6.9) ===
# 持仓>24h仍无盈利→降级衰减
decay_cfg = rules.get("tracker", {}).get("time_decay", {})
if decay_cfg.get("enabled", True):
decay_hours = decay_cfg.get("decay_hours", 24)
rec_time = rec.get("rec_time", "")
if rec_time:
try:
t_rec = datetime.fromisoformat(rec_time)
hours_held = (datetime.now() - t_rec).total_seconds() / 3600
if hours_held > decay_hours and pnl_pct <= 0 and action_status == "持有":
sell_signals.append(f"⏰ 持仓{hours_held:.0f}h无盈利降级衰减")
action_status = "衰减"
except Exception:
pass
# ---- 动态买入指引(只允许在未触发任何止盈/止损/退出信号时执行) ----
entry_update = {}
current_action = entry_plan.get("entry_action", "")
# 如果推荐状态是"等回踩",检查是否到了回踩价位。
# 注意:一旦本轮 action_status 已经是止盈/止损/跟踪止盈/反转/衰减,就绝不能再覆盖成“可即刻买入”。
if action_status == "持有" and current_action in ("等回踩", "🟡等回踩") and h4_df is not None and atr_1h > 0:
pa_4h = full_pa_analysis(h4_df, "4h")
h4_zones = pa_4h.get("zones", [])
direction = 1 # 做多方向
# 重新做15min入场点分析
if m15_df is not None and len(m15_df) >= 20:
entry_result = analyze_entry_point(
h1_df=h1_df, m15_df=m15_df, atr_1h=atr_1h,
zones_4h=h4_zones, direction=direction,
)
new_action = entry_result.get("action", "等回踩")
if new_action == "即刻买入":
buy_signals.append(f"🟢 回踩确认完毕!可即刻入场(15min动K确认)")
action_status = "可即刻买入"
elif new_action == "等回踩":
wait_price = entry_result.get("wait_price", 0)
if wait_price > 0:
# 检查当前价是否接近回踩目标
dist_pct = ((current_price / wait_price) - 1) * 100
if abs(dist_pct) < 2:
buy_signals.append(f"🟢 当前价接近回踩目标!${current_price:.4f}≈${wait_price:.4f}")
action_status = "可即刻买入"
entry_update = {
"new_action": new_action,
"reason": entry_result.get("reason", ""),
"wait_price": entry_result.get("wait_price", 0),
}
action_status, gated_plan, gate_reasons = apply_entry_quality_gate(
action_status=action_status,
entry_plan=entry_plan,
signals=rec.get("signals"),
current_price=current_price,
market_context=rec.get("market_context") or {},
derivatives_context=rec.get("derivatives_context") or {},
sector_context=rec.get("sector_context") or {},
)
if gate_reasons:
buy_signals.append("⚠️ 买点质量闸门: " + "".join(gate_reasons[:3]))
entry_plan.update(gated_plan)
return {
"action_status": action_status,
"sell_signals": sell_signals,
"buy_signals": buy_signals,
"exhaustion": exhaustion,
"entry_update": entry_update,
"pnl_pct": round(pnl_pct, 2),
"trailing_stop_level": trailing_stop_level,
}
def track_prices():
"""拉取所有active推荐币的实时价格更新盈亏 + 动态跟踪信号"""
recs = get_active_recommendations()
if not recs:
output = {
"status": "no_active",
"message": "无active推荐需要跟踪",
"stats": get_stats(),
"track_time": datetime.now().isoformat(),
}
print(json.dumps(output, ensure_ascii=False))
return output
results = []
failed_symbols = []
for rec in recs:
symbol = rec["symbol"]
try:
ticker = exchange.fetch_ticker(symbol)
current_price = ticker["last"]
# 最新价格缓存:看板读取小表 latest_price_cache不再依赖 price_tracking 高频流水表
update_latest_price_cache(symbol, current_price, source="tracker")
# 基础盈亏跟踪
track_result = update_recommendation_tracking(rec["id"], current_price)
# PA增强动态跟踪信号分析
tracking_signals = analyze_tracking_signals(symbol, rec, current_price)
# === v1.7.8 跟踪止盈DB回写 ===
# 每次tracker运行都写DB,支持动态跟随行情逐步抬高跟踪位
trail_level = tracking_signals.get("trailing_stop_level", 0)
if trail_level > 0:
entry_plan = rec.get("entry_plan") or {}
old_trail = entry_plan.get("trailing_stop_level", 0)
# v1.7.8: 只要跟踪位有变化就写DB(上移时必变;首次激活也写)
if abs(trail_level - old_trail) > 0.000001:
entry_plan["trailing_stop_level"] = trail_level
import sqlite3 as _sq
_c2 = _sq.connect(os.getenv("ALPHAX_DB_PATH", os.path.join(os.path.dirname(__file__), "data", "altcoin_monitor.db")))
_c2.execute("UPDATE recommendation SET entry_plan_json=? WHERE id=?",
(json.dumps(entry_plan, ensure_ascii=False), rec["id"]))
_c2.commit()
_c2.close()
# 主链路状态迁移tracker 只提交“候选状态 + 当前价”,最终状态由 DB 主链路统一落库。
# 飞书推送只能消费主链路返回的最终状态,不能再自行判断。
terminal_action = {
"hit_tp2": "止盈2",
"stopped_out": "止损",
}.get(track_result.get("status"))
requested_action = terminal_action or tracking_signals["action_status"]
state_decision = apply_recommendation_state_transition(
rec["id"],
requested_action=requested_action,
current_price=current_price,
event_time=datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
signals=tracking_signals.get("sell_signals", []) + tracking_signals.get("buy_signals", []),
)
final_action = state_decision.get("action_status", requested_action)
if state_decision.get("push_required"):
if should_push(symbol, "entry", final_action):
try:
push_altcoin_tp_sl_alert(
state_decision["push_symbol"],
state_decision["push_current_price"],
state_decision["push_entry_price"],
state_decision["push_pnl_pct"],
final_action,
state_decision.get("push_signals", []),
state_decision.get("stop_loss", 0),
state_decision.get("tp1", 0),
state_decision.get("tp2", 0),
)
log_push(symbol, "entry", final_action, rec_id=rec["id"])
except Exception as e:
print(f"飞书推送失败({symbol}): {e}")
else:
print(f"⏭ 跳过推送({symbol}): entry/{final_action} 12h冷却中")
results.append({
"symbol": symbol,
"rec_id": rec["id"],
"entry_price": rec["entry_price"],
"current_price": current_price,
"pnl_pct": tracking_signals["pnl_pct"],
"status": track_result["status"],
"action_status": tracking_signals["action_status"],
"sell_signals": tracking_signals["sell_signals"],
"buy_signals": tracking_signals["buy_signals"],
"exhaustion_severity": tracking_signals.get("exhaustion", {}).get("severity", "low"),
})
print(f" {symbol}: 入场${rec['entry_price']} → 现在${current_price} "
f"盈亏{tracking_signals['pnl_pct']}% 状态={track_result['status']} "
f"操作={tracking_signals['action_status']}")
except Exception as e:
failed_symbols.append({"symbol": symbol, "error": str(e)})
print(f" {symbol}: 获取价格失败 - {e}")
# 过期检查
expire_old_recommendations()
output = {
"status": "tracked",
"tracked_count": len(results),
"failed_count": len(failed_symbols),
"failed_symbols": failed_symbols,
"results": results,
"stats": get_stats(),
"track_time": datetime.now().isoformat(),
}
print(json.dumps(output, ensure_ascii=False, indent=2))
return output
if __name__ == "__main__":
started_at = datetime.now()
try:
init_db()
output = track_prices()
except Exception as e:
finished_at = datetime.now()
log_cron_run(
job_name="跟踪",
script_name="price_tracker.py",
run_status="error",
result_status="exception",
started_at=started_at.isoformat(),
finished_at=finished_at.isoformat(),
duration_ms=int((finished_at - started_at).total_seconds() * 1000),
summary={},
error_message=str(e),
)
raise
else:
finished_at = datetime.now()
summary = {
"tracked_count": output.get("tracked_count", 0),
"failed_count": output.get("failed_count", 0),
"active_count": output.get("stats", {}).get("active_count", 0),
}
log_cron_run(
job_name="跟踪",
script_name="price_tracker.py",
run_status="success",
result_status=output.get("status", "completed"),
started_at=started_at.isoformat(),
finished_at=finished_at.isoformat(),
duration_ms=int((finished_at - started_at).total_seconds() * 1000),
summary=summary,
error_message="",
)