feat: linear FR scoring based on historical max

- FR score = (|current_FR| / max_historical_FR) × 5, clamped to [-5, +5]
- Contrarian direction: positive score (going against crowded side)
- Same direction: negative score (going with crowded side)
- Historical max FR cached per symbol, refreshed every hour
- Score is float with 2 decimal places
- Total score supports 1 decimal place
This commit is contained in:
root 2026-03-02 00:56:22 +00:00
parent e405a9c21e
commit 1fa6f178b6
2 changed files with 58 additions and 16 deletions

View File

@ -256,6 +256,31 @@ class ATRCalculator:
return (rank / len(sorted_hist)) * 100
# ─── FR历史最大值缓存每小时更新───────────────────────────────
_max_fr_cache: dict = {} # {symbol: max_abs_fr}
_max_fr_updated: float = 0
def get_max_fr(symbol: str) -> float:
"""获取该币种历史最大|FR|,每小时刷新一次"""
global _max_fr_cache, _max_fr_updated
now = time.time()
if now - _max_fr_updated > 3600 or symbol not in _max_fr_cache:
try:
with get_sync_conn() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT symbol, MAX(ABS((value->>'fundingRate')::float)) as max_fr "
"FROM market_indicators WHERE indicator_type='funding_rate' "
"GROUP BY symbol"
)
for row in cur.fetchall():
_max_fr_cache[row[0]] = row[1] if row[1] else 0.0001
_max_fr_updated = now
except Exception as e:
logger.warning(f"get_max_fr error: {e}")
return _max_fr_cache.get(symbol, 0.0001) # 默认0.01%防除零
class SymbolState:
def __init__(self, symbol: str):
self.symbol = symbol
@ -491,21 +516,19 @@ class SymbolState:
# Funding Rate scoring (拥挤层加分)
# Read from market_indicators table
funding_rate = to_float(self.market_indicators.get("funding_rate"))
fr_score = 0
if "funding_rate" in enabled_signals and funding_rate is not None:
fr_score = 0.0
if "funding_rate" in enabled_signals and funding_rate is not None and funding_rate != 0:
max_fr = get_max_fr(self.symbol)
fr_abs = abs(funding_rate)
if fr_abs >= 0.0001: # extreme ±0.01% — FR明显偏移
# 做多但FR高多头拥挤付费= 危险
if (direction == "LONG" and funding_rate > 0) or (direction == "SHORT" and funding_rate < 0):
fr_score = -5
else:
fr_score = 5 # 反向操作,加分
elif fr_abs >= 0.00003: # moderate ±0.003% — 轻度偏移
# 做多且FR为负空头付钱给多头= 顺势
if (direction == "LONG" and funding_rate < 0) or (direction == "SHORT" and funding_rate > 0):
fr_score = 5
else:
fr_score = 0
# 线性映射: raw = (|当前FR| / 历史最大FR) × 5, clamp到5
raw_score = min(fr_abs / max_fr * 5.0, 5.0)
# 方向判断: 逆向操作加分,顺向操作减分
# 做多+FR负(空头付钱)=逆向=好 → 正分
# 做多+FR正(多头付钱)=顺向=坏 → 负分
if (direction == "LONG" and funding_rate < 0) or (direction == "SHORT" and funding_rate > 0):
fr_score = round(raw_score, 2) # 逆向操作,加分
else:
fr_score = round(-raw_score, 2) # 顺向操作,减分
# 4) 确认层15分
confirmation_score = 15 if (
@ -578,7 +601,7 @@ class SymbolState:
scaled_auxiliary = round(aux_score / 5 * w_auxiliary)
total_score = scaled_direction + scaled_crowding + scaled_fr + scaled_environment + scaled_confirmation + scaled_liq + scaled_auxiliary
total_score = max(0, min(total_score, 100)) # clamp 0~100
total_score = max(0, min(round(total_score, 1), 100)) # clamp 0~100, 保留1位小数
result["score"] = total_score
result["direction"] = direction
@ -595,7 +618,7 @@ class SymbolState:
"environment": {"score": scaled_environment, "max": w_environment, "open_interest_hist": oi_change},
"confirmation": {"score": scaled_confirmation, "max": w_confirmation},
"auxiliary": {"score": scaled_auxiliary, "max": w_auxiliary, "coinbase_premium": coinbase_premium},
"funding_rate": {"score": scaled_fr, "max": w_fr, "value": funding_rate},
"funding_rate": {"score": round(scaled_fr, 2), "max": w_fr, "value": funding_rate, "max_fr_hist": get_max_fr(self.symbol)},
"liquidation": {
"score": scaled_liq,
"max": w_liq,

View File

@ -139,3 +139,22 @@
2026-03-02 00:34:32,236 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=84 price=83.8
2026-03-02 00:35:03,725 [INFO] signal-engine: 冷启动保护期结束,模拟盘开仓已启用
2026-03-02 00:38:28,367 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v52_8signals]: LONG score=75 price=1.4
2026-03-02 00:44:45,649 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=90 price=66018.3
2026-03-02 00:44:45,649 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=83 price=66018.3
2026-03-02 00:44:45,939 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v51_baseline]: LONG score=90 price=1947.1
2026-03-02 00:44:45,940 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v52_8signals]: LONG score=83 price=1947.1
2026-03-02 00:44:46,036 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v51_baseline]: LONG score=82 price=1.4
2026-03-02 00:44:46,128 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=87 price=84.0
2026-03-02 00:44:46,129 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=80 price=84.0
2026-03-02 00:48:42,855 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v52_8signals]: LONG score=75 price=1.4
2026-03-02 00:50:01,898 [INFO] signal-engine: [BTCUSDT] 状态: CVD_fast=587.6 CVD_mid=5583.3 ATR=230.66 (69%) VWAP=66328.7
2026-03-02 00:50:01,898 [INFO] signal-engine: [ETHUSDT] 状态: CVD_fast=13374.7 CVD_mid=74135.1 ATR=7.58 (69%) VWAP=1953.2
2026-03-02 00:50:01,899 [INFO] signal-engine: [XRPUSDT] 状态: CVD_fast=686323.2 CVD_mid=4294786.9 ATR=0.00 (66%) VWAP=1.4
2026-03-02 00:50:01,899 [INFO] signal-engine: [SOLUSDT] 状态: CVD_fast=159663.5 CVD_mid=524572.1 ATR=0.37 (38%) VWAP=84.4
2026-03-02 00:55:01,241 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v51_baseline]: LONG score=85 price=66391.1
2026-03-02 00:55:01,241 [INFO] signal-engine: [BTCUSDT] 🚨 信号[v52_8signals]: LONG score=83 price=66391.1
2026-03-02 00:55:01,565 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v51_baseline]: LONG score=90 price=1954.9
2026-03-02 00:55:01,565 [INFO] signal-engine: [ETHUSDT] 🚨 信号[v52_8signals]: LONG score=83 price=1954.9
2026-03-02 00:55:01,681 [INFO] signal-engine: [XRPUSDT] 🚨 信号[v51_baseline]: LONG score=77 price=1.4
2026-03-02 00:55:01,797 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v51_baseline]: LONG score=87 price=84.5
2026-03-02 00:55:01,797 [INFO] signal-engine: [SOLUSDT] 🚨 信号[v52_8signals]: LONG score=75 price=84.5