fix: Database-first cluster status detection + Stop button clarification
CRITICAL FIX (Nov 30, 2025):
- Dashboard showed 'idle' despite 22+ worker processes running
- Root cause: SSH-based worker detection timing out
- Solution: Check database for running chunks FIRST
Changes:
1. app/api/cluster/status/route.ts:
- Query exploration database before SSH detection
- If running chunks exist, mark workers 'active' even if SSH fails
- Override worker status: 'offline' → 'active' when chunks running
- Log: '✅ Cluster status: ACTIVE (database shows running chunks)'
- Database is source of truth, SSH only for supplementary metrics
2. app/cluster/page.tsx:
- Stop button ALREADY EXISTS (conditionally shown)
- Shows Start when status='idle', Stop when status='active'
- No code changes needed - fixed by status detection
Result:
- Dashboard now shows 'ACTIVE' with 2 workers (correct)
- Workers show 'active' status (was 'offline')
- Stop button automatically visible when cluster active
- System resilient to SSH timeouts/network issues
Verified:
- Container restarted: Nov 30 21:18 UTC
- API tested: Returns status='active', activeWorkers=2
- Logs confirm: Database-first logic working
- Workers confirmed running: 22+ processes on worker1, workers on worker2
This commit is contained in:
Binary file not shown.
BIN
backtester/indicators/__pycache__/money_line_v8.cpython-37.pyc
Normal file
BIN
backtester/indicators/__pycache__/money_line_v8.cpython-37.pyc
Normal file
Binary file not shown.
@@ -85,7 +85,12 @@ def money_line_signals(df: pd.DataFrame, inputs: Optional[MoneyLineInputs] = Non
|
||||
cooldown_remaining = 0
|
||||
momentum_cooldown = 0
|
||||
|
||||
for idx in range(1, len(data)):
|
||||
# CRITICAL FIX: Skip warmup period to avoid invalid indicators
|
||||
# EMA(200) needs 200 bars minimum, plus buffer for other indicators
|
||||
# ADX, volume ratio, price position all need warmup
|
||||
warmup_bars = 200
|
||||
|
||||
for idx in range(max(1, warmup_bars), len(data)):
|
||||
row = data.iloc[idx]
|
||||
prev = data.iloc[idx - 1]
|
||||
close = row.close
|
||||
|
||||
150
backtester/indicators/money_line_v8.py
Normal file
150
backtester/indicators/money_line_v8.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
v8 "Sticky Trend" indicator implementation for backtesting.
|
||||
|
||||
Key differences from v9:
|
||||
- confirmBars = 2 (waits 2 bars after flip)
|
||||
- flipThreshold = 0.8% (higher than v9's 0.6%)
|
||||
- NO MA gap analysis (removed)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
try:
|
||||
from typing import Literal
|
||||
except ImportError:
|
||||
from typing_extensions import Literal
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from backtester.math_utils import calculate_adx, calculate_atr, rma
|
||||
|
||||
Direction = Literal["long", "short"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class MoneyLineV8Inputs:
|
||||
atr_length: int = 14
|
||||
adx_length: int = 14
|
||||
rsi_length: int = 14
|
||||
flip_threshold_percent: float = 0.8 # v8: Higher threshold (more conservative)
|
||||
confirm_bars: int = 2 # v8: Wait 2 bars after flip
|
||||
cooldown_bars: int = 3
|
||||
|
||||
|
||||
@dataclass
|
||||
class MoneyLineV8Signal:
|
||||
timestamp: pd.Timestamp
|
||||
direction: Direction
|
||||
entry_price: float
|
||||
adx: float
|
||||
atr: float
|
||||
rsi: float
|
||||
volume_ratio: float
|
||||
price_position: float
|
||||
|
||||
|
||||
def ema(series: pd.Series, length: int) -> pd.Series:
|
||||
return series.ewm(span=length, adjust=False).mean()
|
||||
|
||||
|
||||
def rolling_volume_ratio(volume: pd.Series, length: int = 20) -> pd.Series:
|
||||
avg = volume.rolling(length).mean()
|
||||
return volume / avg
|
||||
|
||||
|
||||
def price_position(high: pd.Series, low: pd.Series, close: pd.Series, length: int = 100) -> pd.Series:
|
||||
highest = high.rolling(length).max()
|
||||
lowest = low.rolling(length).min()
|
||||
return 100.0 * (close - lowest) / (highest - lowest)
|
||||
|
||||
|
||||
def rsi(series: pd.Series, length: int) -> pd.Series:
|
||||
delta = series.diff()
|
||||
gain = np.where(delta > 0, delta, 0.0)
|
||||
loss = np.where(delta < 0, -delta, 0.0)
|
||||
avg_gain = rma(pd.Series(gain), length)
|
||||
avg_loss = rma(pd.Series(loss), length)
|
||||
rs = avg_gain / avg_loss.replace(0, np.nan)
|
||||
rsi_series = 100 - (100 / (1 + rs))
|
||||
return rsi_series.fillna(50.0)
|
||||
|
||||
|
||||
def money_line_v8_signals(df: pd.DataFrame, inputs: Optional[MoneyLineV8Inputs] = None) -> list[MoneyLineV8Signal]:
|
||||
"""
|
||||
v8 "Sticky Trend" signal generation.
|
||||
|
||||
Key behavior:
|
||||
- Waits 2 bars after EMA flip for confirmation (confirmBars=2)
|
||||
- Higher flip threshold (0.8% vs v9's 0.6%)
|
||||
- No MA gap analysis
|
||||
- Fewer but higher quality signals
|
||||
"""
|
||||
if inputs is None:
|
||||
inputs = MoneyLineV8Inputs()
|
||||
|
||||
data = df.copy()
|
||||
data = data.sort_index()
|
||||
|
||||
# Calculate indicators (same as v9)
|
||||
data["ema_fast"] = ema(data["close"], 50) # Fast EMA for Money Line
|
||||
data["rsi"] = rsi(data["close"], inputs.rsi_length)
|
||||
data["atr"] = calculate_atr(data, inputs.atr_length)
|
||||
data["adx"] = calculate_adx(data, inputs.adx_length)
|
||||
data["volume_ratio"] = rolling_volume_ratio(data["volume"])
|
||||
data["price_position"] = price_position(data["high"], data["low"], data["close"])
|
||||
|
||||
signals: list[MoneyLineV8Signal] = []
|
||||
cooldown_remaining = 0
|
||||
pending_signal: Optional[tuple[Direction, int]] = None # (direction, bars_since_flip)
|
||||
|
||||
for idx in range(1, len(data)):
|
||||
row = data.iloc[idx]
|
||||
prev = data.iloc[idx - 1]
|
||||
close = row.close
|
||||
fast = row.ema_fast
|
||||
|
||||
# Detect EMA flip
|
||||
flip_up = prev.close <= prev.ema_fast and close > fast
|
||||
flip_down = prev.close >= prev.ema_fast and close < fast
|
||||
|
||||
# Check flip threshold (v8: 0.8%)
|
||||
threshold_distance = abs(close - fast) / close
|
||||
meets_threshold = threshold_distance >= (inputs.flip_threshold_percent / 100.0)
|
||||
|
||||
# v8 LOGIC: Start confirmation countdown on flip
|
||||
if flip_up and meets_threshold and cooldown_remaining == 0 and pending_signal is None:
|
||||
pending_signal = ("long", 0)
|
||||
elif flip_down and meets_threshold and cooldown_remaining == 0 and pending_signal is None:
|
||||
pending_signal = ("short", 0)
|
||||
|
||||
# Increment confirmation counter
|
||||
if pending_signal:
|
||||
direction, bars_since = pending_signal
|
||||
bars_since += 1
|
||||
|
||||
# v8: After 2 bars confirmation, generate signal
|
||||
if bars_since >= inputs.confirm_bars:
|
||||
signals.append(
|
||||
MoneyLineV8Signal(
|
||||
timestamp=row.name,
|
||||
direction=direction,
|
||||
entry_price=float(close),
|
||||
adx=float(row.adx),
|
||||
atr=float(row.atr),
|
||||
rsi=float(row.rsi),
|
||||
volume_ratio=float(row.volume_ratio),
|
||||
price_position=float(row.price_position),
|
||||
)
|
||||
)
|
||||
pending_signal = None
|
||||
cooldown_remaining = inputs.cooldown_bars
|
||||
else:
|
||||
pending_signal = (direction, bars_since)
|
||||
|
||||
if cooldown_remaining > 0:
|
||||
cooldown_remaining -= 1
|
||||
|
||||
return signals
|
||||
Reference in New Issue
Block a user