fix: Add TP1 TP2 SL prices to Telegram trade notifications
This commit is contained in:
@@ -119,19 +119,25 @@ export async function GET() {
|
||||
})
|
||||
|
||||
// Define version metadata FIRST (before usage)
|
||||
// Updated Jan 3, 2026 - Added v9, v11, v11.2opt support
|
||||
const versionDescriptions: Record<string, string> = {
|
||||
'v8': 'Money Line Sticky Trend (Nov 18+) - PRODUCTION',
|
||||
'v11.2opt': 'Money Line v11.2 OPTIMIZED (Dec 26+) - PRODUCTION',
|
||||
'v11.2': 'Money Line v11.2 INDICATOR (Dec 26+)',
|
||||
'v11.1': 'Money Line v11.1 Fixed Filters (Dec 15+)',
|
||||
'v11': 'Money Line v11 All Filters (Dec 15+)',
|
||||
'v9': 'Money Line v9 MA Gap + Momentum (Nov 26+) - ARCHIVED',
|
||||
'v8': 'Money Line Sticky Trend (Nov 18-26) - ARCHIVED',
|
||||
'v7': 'HalfTrend with toggles (deprecated)',
|
||||
'v6': 'HalfTrend + BarColor (Nov 12-18) - ARCHIVED',
|
||||
'v5': 'Buy/Sell Signal (pre-Nov 12) - ARCHIVED',
|
||||
'unknown': 'No version tracked (pre-Nov 12) - ARCHIVED'
|
||||
}
|
||||
|
||||
const archivedVersions = ['v5', 'v6', 'v7', 'unknown']
|
||||
const archivedVersions = ['v5', 'v6', 'v7', 'v8', 'v9', 'unknown']
|
||||
|
||||
// Sort versions: v8 first (production), then v7, v6, v5, unknown (archived)
|
||||
// Sort versions: v11.2opt first (production), then descending by version
|
||||
const versionOrder: Record<string, number> = {
|
||||
'v8': 0, 'v7': 1, 'v6': 2, 'v5': 3, 'unknown': 4
|
||||
'v11.2opt': 0, 'v11.2': 1, 'v11.1': 2, 'v11': 3, 'v9': 4, 'v8': 5, 'v7': 6, 'v6': 7, 'v5': 8, 'unknown': 9
|
||||
}
|
||||
results.sort((a, b) => {
|
||||
const orderA = versionOrder[a.version] ?? 999
|
||||
@@ -149,7 +155,7 @@ export async function GET() {
|
||||
success: true,
|
||||
versions: resultsWithArchived,
|
||||
descriptions: versionDescriptions,
|
||||
production: 'v8',
|
||||
production: 'v11.2opt',
|
||||
archived: archivedVersions,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
|
||||
766
backtester/v11_shorts_only.py
Normal file
766
backtester/v11_shorts_only.py
Normal file
@@ -0,0 +1,766 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
V11 Money Line - SHORTS ONLY Backtester
|
||||
|
||||
PURPOSE: Optimize SHORT signal parameters WITHOUT touching LONG settings.
|
||||
|
||||
This is a SEPARATE file from v11_moneyline_all_filters.py to avoid accidentally
|
||||
changing the proven LONG settings.
|
||||
|
||||
Current LONG settings (DO NOT CHANGE - these are proven profitable):
|
||||
- ATR Period: 12, Multiplier: 3.8
|
||||
- ADX Min: 15 (ADX Length: 17)
|
||||
- RSI Long: 56-69
|
||||
- Long Position Max: 85%
|
||||
- Entry Buffer: -0.15 ATR (early entry)
|
||||
- Volume Filter: OFF
|
||||
|
||||
SHORT settings to optimize (this is what we're sweeping):
|
||||
- RSI Short Min/Max (currently 30-70 - NEEDS OPTIMIZATION)
|
||||
- Short Position Min (currently 5% - way too low)
|
||||
- ADX Min for shorts (may differ from longs)
|
||||
- Entry Buffer for shorts
|
||||
- Volume filter for shorts
|
||||
|
||||
Usage:
|
||||
python3 v11_shorts_only.py --sweep # Run parameter sweep for shorts
|
||||
python3 v11_shorts_only.py --baseline # Test with default short settings
|
||||
python3 v11_shorts_only.py --custom --rsi-min 40 --rsi-max 70 --pos-min 25
|
||||
|
||||
Created: Jan 6, 2026
|
||||
Author: Trading Bot System
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Callable, List, Tuple
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import argparse
|
||||
import sys
|
||||
import csv
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import itertools
|
||||
import multiprocessing as mp
|
||||
|
||||
|
||||
#======================================================================
|
||||
# TRADE CONFIG AND RESULTS (self-contained)
|
||||
#======================================================================
|
||||
|
||||
@dataclass
|
||||
class ShortTradeConfig:
|
||||
"""Trading configuration for shorts simulation."""
|
||||
position_size: float = 1000.0
|
||||
take_profit_1_size_percent: float = 100.0 # TP1 closes 100% for shorts
|
||||
atr_multiplier_tp1: float = 1.45 / 0.43 * (1/2.0) # Targeting ~1.45% TP1
|
||||
atr_multiplier_sl: float = 3.0
|
||||
min_tp1_percent: float = 1.45
|
||||
max_tp1_percent: float = 1.45
|
||||
min_sl_percent: float = 0.8
|
||||
max_sl_percent: float = 2.0
|
||||
max_bars_per_trade: int = 200 # ~16 hours max
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShortTradeResult:
|
||||
"""Result of a single simulated short trade."""
|
||||
entry_time: pd.Timestamp
|
||||
exit_time: pd.Timestamp
|
||||
entry_price: float
|
||||
exit_price: float
|
||||
realized_pnl: float
|
||||
profit_percent: float
|
||||
exit_reason: str
|
||||
bars_held: int
|
||||
tp1_hit: bool
|
||||
mae_percent: float
|
||||
mfe_percent: float
|
||||
adx_at_entry: float
|
||||
atr_at_entry: float
|
||||
rsi_at_entry: float
|
||||
price_position_at_entry: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShortSimulationResult:
|
||||
"""Result of a shorts simulation backtest."""
|
||||
trades: List[ShortTradeResult] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def total_trades(self) -> int:
|
||||
return len(self.trades)
|
||||
|
||||
@property
|
||||
def winning_trades(self) -> int:
|
||||
return sum(1 for t in self.trades if t.realized_pnl > 0)
|
||||
|
||||
@property
|
||||
def losing_trades(self) -> int:
|
||||
return sum(1 for t in self.trades if t.realized_pnl <= 0)
|
||||
|
||||
@property
|
||||
def win_rate(self) -> float:
|
||||
if self.total_trades == 0:
|
||||
return 0.0
|
||||
return self.winning_trades / self.total_trades
|
||||
|
||||
@property
|
||||
def total_pnl(self) -> float:
|
||||
return sum(t.realized_pnl for t in self.trades)
|
||||
|
||||
@property
|
||||
def average_pnl(self) -> float:
|
||||
if self.total_trades == 0:
|
||||
return 0.0
|
||||
return self.total_pnl / self.total_trades
|
||||
|
||||
@property
|
||||
def profit_factor(self) -> float:
|
||||
gross_profit = sum(t.realized_pnl for t in self.trades if t.realized_pnl > 0)
|
||||
gross_loss = abs(sum(t.realized_pnl for t in self.trades if t.realized_pnl <= 0))
|
||||
if gross_loss == 0:
|
||||
return float('inf') if gross_profit > 0 else 0.0
|
||||
return gross_profit / gross_loss
|
||||
|
||||
@property
|
||||
def max_drawdown(self) -> float:
|
||||
if not self.trades:
|
||||
return 0.0
|
||||
cumulative = 0.0
|
||||
peak = 0.0
|
||||
max_dd = 0.0
|
||||
for t in self.trades:
|
||||
cumulative += t.realized_pnl
|
||||
if cumulative > peak:
|
||||
peak = cumulative
|
||||
dd = peak - cumulative
|
||||
if dd > max_dd:
|
||||
max_dd = dd
|
||||
return max_dd
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShortOnlyInputs:
|
||||
"""
|
||||
Parameters for SHORT-ONLY signal generation.
|
||||
|
||||
FIXED parameters (from proven LONG optimization - shared with indicator):
|
||||
- atr_period, multiplier: Supertrend calculation
|
||||
- adx_length: ADX calculation period
|
||||
- flip_threshold, confirm_bars: Trend flip confirmation
|
||||
|
||||
SWEEP parameters (what we're optimizing for SHORTs):
|
||||
- adx_min: Minimum ADX for short entries (may differ from long)
|
||||
- rsi_short_min, rsi_short_max: RSI boundaries for shorts
|
||||
- short_pos_min: Minimum price position for shorts (avoid catching falling knives)
|
||||
- entry_buffer_atr: How far beyond line price must be (negative = early entry)
|
||||
- vol_min, vol_max: Volume ratio bounds
|
||||
"""
|
||||
# FIXED - Supertrend/ATR (proven settings, don't sweep)
|
||||
atr_period: int = 12
|
||||
multiplier: float = 3.8
|
||||
adx_length: int = 17
|
||||
rsi_length: int = 14
|
||||
flip_threshold: float = 0.0 # Percentage for flip confirmation
|
||||
confirm_bars: int = 1 # Bars needed to confirm flip
|
||||
cooldown_bars: int = 2 # Bars between signals
|
||||
|
||||
# SWEEP - Short-specific filters (THESE are what we're optimizing)
|
||||
adx_min: float = 15.0 # Minimum ADX for trend strength
|
||||
rsi_short_min: float = 30.0 # RSI minimum for shorts (default 30)
|
||||
rsi_short_max: float = 70.0 # RSI maximum for shorts (default 70)
|
||||
short_pos_min: float = 5.0 # Price position minimum % (default 5 - too low!)
|
||||
entry_buffer_atr: float = -0.15 # ATR multiplier for entry buffer (negative = early)
|
||||
vol_min: float = 0.0 # Minimum volume ratio (0 = disabled)
|
||||
vol_max: float = 5.0 # Maximum volume ratio
|
||||
|
||||
# Control
|
||||
use_quality_filters: bool = True # Apply all filters
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShortSignal:
|
||||
"""A single SHORT signal from the Money Line indicator."""
|
||||
timestamp: pd.Timestamp
|
||||
entry_price: float
|
||||
adx: float
|
||||
atr: float
|
||||
rsi: float
|
||||
volume_ratio: float
|
||||
price_position: float
|
||||
direction: str = "short" # Always short
|
||||
|
||||
|
||||
def ema(series: pd.Series, period: int) -> pd.Series:
|
||||
"""Exponential Moving Average."""
|
||||
return series.ewm(span=period, adjust=False).mean()
|
||||
|
||||
|
||||
def rolling_volume_ratio(volume: pd.Series, period: int = 20) -> pd.Series:
|
||||
"""Volume relative to 20-period SMA."""
|
||||
sma = volume.rolling(window=period).mean()
|
||||
return volume / sma
|
||||
|
||||
|
||||
def price_position(high: pd.Series, low: pd.Series, close: pd.Series, period: int = 100) -> pd.Series:
|
||||
"""Price position within recent range (0-100%)."""
|
||||
highest = high.rolling(window=period).max()
|
||||
lowest = low.rolling(window=period).min()
|
||||
range_val = highest - lowest
|
||||
pos = np.where(range_val > 0, (close - lowest) / range_val * 100, 50.0)
|
||||
return pd.Series(pos, index=close.index)
|
||||
|
||||
|
||||
def rsi(close: pd.Series, period: int = 14) -> pd.Series:
|
||||
"""Relative Strength Index."""
|
||||
delta = close.diff()
|
||||
gain = delta.where(delta > 0, 0.0)
|
||||
loss = (-delta).where(delta < 0, 0.0)
|
||||
avg_gain = gain.ewm(alpha=1/period, min_periods=period, adjust=False).mean()
|
||||
avg_loss = loss.ewm(alpha=1/period, min_periods=period, adjust=False).mean()
|
||||
rs = avg_gain / avg_loss.replace(0, np.nan)
|
||||
return 100 - (100 / (1 + rs))
|
||||
|
||||
|
||||
def calculate_atr(df: pd.DataFrame, period: int = 14) -> pd.Series:
|
||||
"""Average True Range."""
|
||||
high = df['high']
|
||||
low = df['low']
|
||||
close = df['close']
|
||||
|
||||
tr1 = high - low
|
||||
tr2 = abs(high - close.shift(1))
|
||||
tr3 = abs(low - close.shift(1))
|
||||
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
|
||||
|
||||
return tr.rolling(window=period).mean()
|
||||
|
||||
|
||||
def calculate_adx(df: pd.DataFrame, period: int = 14) -> pd.Series:
|
||||
"""Average Directional Index."""
|
||||
high = df['high']
|
||||
low = df['low']
|
||||
close = df['close']
|
||||
|
||||
plus_dm = high.diff()
|
||||
minus_dm = -low.diff()
|
||||
|
||||
plus_dm = plus_dm.where((plus_dm > minus_dm) & (plus_dm > 0), 0)
|
||||
minus_dm = minus_dm.where((minus_dm > plus_dm) & (minus_dm > 0), 0)
|
||||
|
||||
tr = pd.concat([
|
||||
high - low,
|
||||
abs(high - close.shift(1)),
|
||||
abs(low - close.shift(1))
|
||||
], axis=1).max(axis=1)
|
||||
|
||||
atr = tr.rolling(window=period).mean()
|
||||
plus_di = 100 * (plus_dm.rolling(window=period).mean() / atr)
|
||||
minus_di = 100 * (minus_dm.rolling(window=period).mean() / atr)
|
||||
|
||||
dx = 100 * abs(plus_di - minus_di) / (plus_di + minus_di).replace(0, np.nan)
|
||||
adx = dx.rolling(window=period).mean()
|
||||
|
||||
return adx
|
||||
|
||||
|
||||
def supertrend_shorts(
|
||||
df: pd.DataFrame,
|
||||
atr_period: int = 12,
|
||||
multiplier: float = 3.8,
|
||||
flip_threshold: float = 0.0,
|
||||
confirm_bars: int = 1
|
||||
) -> Tuple[pd.Series, pd.Series]:
|
||||
"""
|
||||
Calculate SuperTrend and trend direction.
|
||||
Optimized numpy version for speed.
|
||||
|
||||
Returns:
|
||||
tsl: Trailing stop line
|
||||
trend: 1 for bullish, -1 for bearish
|
||||
"""
|
||||
close = df['close'].values
|
||||
high = df['high'].values
|
||||
low = df['low'].values
|
||||
n = len(close)
|
||||
|
||||
# Calculate ATR
|
||||
tr = np.maximum(
|
||||
high - low,
|
||||
np.maximum(
|
||||
np.abs(high - np.roll(close, 1)),
|
||||
np.abs(low - np.roll(close, 1))
|
||||
)
|
||||
)
|
||||
tr[0] = high[0] - low[0]
|
||||
|
||||
atr = pd.Series(tr).rolling(window=atr_period).mean().values
|
||||
src = (high + low) / 2
|
||||
|
||||
up = src - (multiplier * atr)
|
||||
dn = src + (multiplier * atr)
|
||||
|
||||
up1 = up.copy()
|
||||
dn1 = dn.copy()
|
||||
trend = np.ones(n, dtype=int)
|
||||
tsl = up1.copy()
|
||||
|
||||
bull_momentum = np.zeros(n, dtype=int)
|
||||
bear_momentum = np.zeros(n, dtype=int)
|
||||
|
||||
threshold = flip_threshold / 100.0
|
||||
confirm_threshold = confirm_bars + 1
|
||||
|
||||
for i in range(1, n):
|
||||
if close[i-1] > up1[i-1]:
|
||||
up1[i] = max(up[i], up1[i-1])
|
||||
else:
|
||||
up1[i] = up[i]
|
||||
|
||||
if close[i-1] < dn1[i-1]:
|
||||
dn1[i] = min(dn[i], dn1[i-1])
|
||||
else:
|
||||
dn1[i] = dn[i]
|
||||
|
||||
if trend[i-1] == 1:
|
||||
tsl[i] = max(up1[i], tsl[i-1])
|
||||
else:
|
||||
tsl[i] = min(dn1[i], tsl[i-1])
|
||||
|
||||
threshold_amount = tsl[i] * threshold
|
||||
|
||||
if trend[i-1] == 1:
|
||||
if close[i] < (tsl[i] - threshold_amount):
|
||||
bear_momentum[i] = bear_momentum[i-1] + 1
|
||||
bull_momentum[i] = 0
|
||||
else:
|
||||
bear_momentum[i] = 0
|
||||
bull_momentum[i] = 0
|
||||
trend[i] = -1 if bear_momentum[i] >= confirm_threshold else 1
|
||||
else:
|
||||
if close[i] > (tsl[i] + threshold_amount):
|
||||
bull_momentum[i] = bull_momentum[i-1] + 1
|
||||
bear_momentum[i] = 0
|
||||
else:
|
||||
bull_momentum[i] = 0
|
||||
bear_momentum[i] = 0
|
||||
trend[i] = 1 if bull_momentum[i] >= confirm_threshold else -1
|
||||
|
||||
return pd.Series(tsl, index=df.index), pd.Series(trend, index=df.index)
|
||||
|
||||
|
||||
def generate_short_signals(
|
||||
df: pd.DataFrame,
|
||||
inputs: Optional[ShortOnlyInputs] = None
|
||||
) -> Tuple[List[ShortSignal], pd.DataFrame]:
|
||||
"""
|
||||
Generate SHORT-ONLY signals from Money Line indicator.
|
||||
|
||||
IGNORES all LONG signals completely - we only care about SHORTs here.
|
||||
"""
|
||||
if inputs is None:
|
||||
inputs = ShortOnlyInputs()
|
||||
|
||||
data = df.sort_index().copy()
|
||||
|
||||
# Calculate SuperTrend
|
||||
supertrend, trend = supertrend_shorts(
|
||||
data,
|
||||
inputs.atr_period,
|
||||
inputs.multiplier,
|
||||
inputs.flip_threshold,
|
||||
inputs.confirm_bars
|
||||
)
|
||||
data['supertrend'] = supertrend
|
||||
data['trend'] = trend
|
||||
|
||||
# Calculate indicators
|
||||
data["rsi"] = rsi(data["close"], inputs.rsi_length)
|
||||
data["atr"] = calculate_atr(data, inputs.atr_period)
|
||||
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[ShortSignal] = []
|
||||
cooldown_remaining = 0
|
||||
warmup_bars = 200
|
||||
|
||||
for idx in range(max(1, warmup_bars), len(data)):
|
||||
row = data.iloc[idx]
|
||||
prev = data.iloc[idx - 1]
|
||||
|
||||
# ONLY detect SHORT signals (trend flip from bullish to bearish)
|
||||
flip_short = prev.trend == 1 and row.trend == -1
|
||||
|
||||
if not flip_short:
|
||||
continue
|
||||
|
||||
if cooldown_remaining > 0:
|
||||
cooldown_remaining -= 1
|
||||
continue
|
||||
|
||||
# Apply quality filters if enabled
|
||||
if not inputs.use_quality_filters:
|
||||
# No filters - just trend flip
|
||||
signals.append(
|
||||
ShortSignal(
|
||||
timestamp=row.name,
|
||||
entry_price=float(row.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),
|
||||
)
|
||||
)
|
||||
cooldown_remaining = inputs.cooldown_bars
|
||||
continue
|
||||
|
||||
# === ALL FILTERS FOR SHORTS ===
|
||||
|
||||
# ADX filter (trend strength)
|
||||
if inputs.adx_min > 0:
|
||||
adx_ok = row.adx >= inputs.adx_min
|
||||
else:
|
||||
adx_ok = True
|
||||
|
||||
# Volume filter
|
||||
if inputs.vol_min > 0:
|
||||
volume_ok = inputs.vol_min <= row.volume_ratio <= inputs.vol_max
|
||||
else:
|
||||
volume_ok = row.volume_ratio <= inputs.vol_max
|
||||
|
||||
# Entry buffer check (price must be below line by X*ATR)
|
||||
if inputs.entry_buffer_atr != 0:
|
||||
# Negative entry_buffer_atr means early entry (price doesn't need to be far below)
|
||||
entry_buffer_ok = row.close < (row.supertrend - inputs.entry_buffer_atr * row.atr)
|
||||
else:
|
||||
entry_buffer_ok = True
|
||||
|
||||
# RSI filter for shorts
|
||||
rsi_ok = inputs.rsi_short_min <= row.rsi <= inputs.rsi_short_max
|
||||
|
||||
# Price position filter (avoid catching falling knives at bottom)
|
||||
if inputs.short_pos_min > 0:
|
||||
pos_ok = row.price_position > inputs.short_pos_min
|
||||
else:
|
||||
pos_ok = True
|
||||
|
||||
# ALL filters must pass
|
||||
if adx_ok and volume_ok and rsi_ok and pos_ok and entry_buffer_ok:
|
||||
signals.append(
|
||||
ShortSignal(
|
||||
timestamp=row.name,
|
||||
entry_price=float(row.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),
|
||||
)
|
||||
)
|
||||
cooldown_remaining = inputs.cooldown_bars
|
||||
|
||||
return signals, data
|
||||
|
||||
|
||||
def simulate_shorts_only(
|
||||
df: pd.DataFrame,
|
||||
symbol: str,
|
||||
inputs: Optional[ShortOnlyInputs] = None,
|
||||
config: Optional[TradeConfig] = None,
|
||||
) -> SimulationResult:
|
||||
"""
|
||||
Run backtest simulation for SHORT signals only.
|
||||
"""
|
||||
if inputs is None:
|
||||
inputs = ShortOnlyInputs()
|
||||
if config is None:
|
||||
config = TradeConfig()
|
||||
|
||||
data = df.sort_index().copy()
|
||||
index_positions = {ts: idx for idx, ts in enumerate(data.index)}
|
||||
|
||||
signals, enriched = generate_short_signals(data, inputs)
|
||||
|
||||
trades = []
|
||||
next_available_index = 0
|
||||
|
||||
for signal in signals:
|
||||
start_idx = index_positions.get(signal.timestamp)
|
||||
if start_idx is None or start_idx < next_available_index:
|
||||
continue
|
||||
|
||||
# Convert ShortSignal to format expected by simulator
|
||||
from backtester.v11_moneyline_all_filters import MoneyLineV11Signal
|
||||
v11_signal = MoneyLineV11Signal(
|
||||
timestamp=signal.timestamp,
|
||||
direction="short",
|
||||
entry_price=signal.entry_price,
|
||||
adx=signal.adx,
|
||||
atr=signal.atr,
|
||||
rsi=signal.rsi,
|
||||
volume_ratio=signal.volume_ratio,
|
||||
price_position=signal.price_position,
|
||||
)
|
||||
|
||||
trade = _simulate_trade(data, start_idx, v11_signal, symbol, config)
|
||||
if trade is None:
|
||||
continue
|
||||
trades.append(trade)
|
||||
next_available_index = trade._exit_index
|
||||
|
||||
return SimulationResult(trades=trades)
|
||||
|
||||
|
||||
def run_short_sweep(df: pd.DataFrame, symbol: str = "SOLUSDT") -> pd.DataFrame:
|
||||
"""
|
||||
Run parameter sweep specifically for SHORT optimization.
|
||||
|
||||
Sweep Parameters:
|
||||
- rsi_short_min: [30, 35, 40, 45]
|
||||
- rsi_short_max: [60, 65, 70, 75]
|
||||
- short_pos_min: [5, 15, 25, 35]
|
||||
- adx_min: [10, 15, 20, 25]
|
||||
- entry_buffer_atr: [-0.15, 0.0, 0.10, 0.20]
|
||||
|
||||
Total combinations: 4 x 4 x 4 x 4 x 4 = 1,024
|
||||
"""
|
||||
|
||||
# Parameter grid for shorts
|
||||
rsi_short_min_values = [30, 35, 40, 45]
|
||||
rsi_short_max_values = [60, 65, 70, 75]
|
||||
short_pos_min_values = [5, 15, 25, 35]
|
||||
adx_min_values = [10, 15, 20, 25]
|
||||
entry_buffer_values = [-0.15, 0.0, 0.10, 0.20]
|
||||
|
||||
results = []
|
||||
total_combos = (
|
||||
len(rsi_short_min_values) *
|
||||
len(rsi_short_max_values) *
|
||||
len(short_pos_min_values) *
|
||||
len(adx_min_values) *
|
||||
len(entry_buffer_values)
|
||||
)
|
||||
|
||||
print(f"🔍 SHORT PARAMETER SWEEP")
|
||||
print(f" Total combinations: {total_combos}")
|
||||
print(f" Symbol: {symbol}")
|
||||
print(f" Data range: {df.index[0]} to {df.index[-1]}")
|
||||
print()
|
||||
|
||||
combo_count = 0
|
||||
|
||||
for rsi_min in rsi_short_min_values:
|
||||
for rsi_max in rsi_short_max_values:
|
||||
# Skip invalid RSI ranges
|
||||
if rsi_min >= rsi_max:
|
||||
continue
|
||||
|
||||
for pos_min in short_pos_min_values:
|
||||
for adx_min in adx_min_values:
|
||||
for entry_buffer in entry_buffer_values:
|
||||
combo_count += 1
|
||||
|
||||
inputs = ShortOnlyInputs(
|
||||
rsi_short_min=rsi_min,
|
||||
rsi_short_max=rsi_max,
|
||||
short_pos_min=pos_min,
|
||||
adx_min=adx_min,
|
||||
entry_buffer_atr=entry_buffer,
|
||||
)
|
||||
|
||||
config = TradeConfig(
|
||||
tp1_percent=1.45, # Same as longs
|
||||
tp1_size=100, # Full close at TP1
|
||||
sl_percent=2.0,
|
||||
)
|
||||
|
||||
result = simulate_shorts_only(df, symbol, inputs, config)
|
||||
|
||||
results.append({
|
||||
'rsi_short_min': rsi_min,
|
||||
'rsi_short_max': rsi_max,
|
||||
'short_pos_min': pos_min,
|
||||
'adx_min': adx_min,
|
||||
'entry_buffer_atr': entry_buffer,
|
||||
'total_trades': result.total_trades,
|
||||
'win_rate': round(result.win_rate * 100, 1),
|
||||
'total_pnl': round(result.total_pnl, 2),
|
||||
'avg_pnl': round(result.average_pnl, 2),
|
||||
'profit_factor': round(result.profit_factor, 3) if result.profit_factor else 0,
|
||||
'max_drawdown': round(result.max_drawdown, 2),
|
||||
})
|
||||
|
||||
if combo_count % 100 == 0:
|
||||
print(f" Progress: {combo_count}/{total_combos} ({100*combo_count/total_combos:.1f}%)")
|
||||
|
||||
results_df = pd.DataFrame(results)
|
||||
|
||||
# Sort by total PnL descending
|
||||
results_df = results_df.sort_values('total_pnl', ascending=False)
|
||||
|
||||
return results_df
|
||||
|
||||
|
||||
def load_data(filepath: str = None) -> pd.DataFrame:
|
||||
"""Load OHLCV data from CSV."""
|
||||
if filepath is None:
|
||||
# Default data location
|
||||
filepath = Path(__file__).parent / "data" / "solusdt_5m.csv"
|
||||
|
||||
print(f"📊 Loading data from: {filepath}")
|
||||
|
||||
df = pd.read_csv(filepath)
|
||||
|
||||
# Handle timestamp column
|
||||
if 'timestamp' in df.columns:
|
||||
df['timestamp'] = pd.to_datetime(df['timestamp'])
|
||||
df = df.set_index('timestamp')
|
||||
elif 'datetime' in df.columns:
|
||||
df['datetime'] = pd.to_datetime(df['datetime'])
|
||||
df = df.set_index('datetime')
|
||||
elif 'time' in df.columns:
|
||||
df['time'] = pd.to_datetime(df['time'])
|
||||
df = df.set_index('time')
|
||||
|
||||
# Ensure required columns
|
||||
required = ['open', 'high', 'low', 'close', 'volume']
|
||||
df.columns = df.columns.str.lower()
|
||||
|
||||
for col in required:
|
||||
if col not in df.columns:
|
||||
raise ValueError(f"Missing required column: {col}")
|
||||
|
||||
print(f" Rows: {len(df):,}")
|
||||
print(f" Date range: {df.index[0]} to {df.index[-1]}")
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='V11 Money Line SHORTS ONLY Backtester')
|
||||
|
||||
parser.add_argument('--sweep', action='store_true',
|
||||
help='Run full parameter sweep for shorts')
|
||||
parser.add_argument('--baseline', action='store_true',
|
||||
help='Test with default short settings')
|
||||
parser.add_argument('--custom', action='store_true',
|
||||
help='Test with custom parameters')
|
||||
|
||||
# Custom parameters
|
||||
parser.add_argument('--rsi-min', type=float, default=30.0,
|
||||
help='RSI Short Minimum (default: 30)')
|
||||
parser.add_argument('--rsi-max', type=float, default=70.0,
|
||||
help='RSI Short Maximum (default: 70)')
|
||||
parser.add_argument('--pos-min', type=float, default=5.0,
|
||||
help='Short Position Minimum %% (default: 5)')
|
||||
parser.add_argument('--adx-min', type=float, default=15.0,
|
||||
help='ADX Minimum (default: 15)')
|
||||
parser.add_argument('--entry-buffer', type=float, default=-0.15,
|
||||
help='Entry Buffer ATR (default: -0.15)')
|
||||
|
||||
parser.add_argument('--data', type=str, default=None,
|
||||
help='Path to OHLCV CSV file')
|
||||
parser.add_argument('--output', type=str, default=None,
|
||||
help='Output CSV file for sweep results')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load data
|
||||
df = load_data(args.data)
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print(" V11 MONEY LINE - SHORTS ONLY BACKTESTER")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
if args.sweep:
|
||||
print("🔍 Running SHORT parameter sweep...")
|
||||
print()
|
||||
|
||||
results = run_short_sweep(df)
|
||||
|
||||
# Save results
|
||||
output_file = args.output or "sweep_v11_shorts_only.csv"
|
||||
results.to_csv(output_file, index=False)
|
||||
print(f"\n💾 Results saved to: {output_file}")
|
||||
|
||||
# Show top 10 results
|
||||
print("\n" + "=" * 60)
|
||||
print(" TOP 10 SHORT CONFIGURATIONS")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
top10 = results.head(10)
|
||||
for i, row in top10.iterrows():
|
||||
print(f"#{results.index.get_loc(i)+1}:")
|
||||
print(f" RSI: {row['rsi_short_min']}-{row['rsi_short_max']}")
|
||||
print(f" Position Min: {row['short_pos_min']}%")
|
||||
print(f" ADX Min: {row['adx_min']}")
|
||||
print(f" Entry Buffer: {row['entry_buffer_atr']}")
|
||||
print(f" Results: {row['total_trades']} trades, {row['win_rate']}% WR, ${row['total_pnl']} PnL")
|
||||
print(f" PF: {row['profit_factor']}, Max DD: ${row['max_drawdown']}")
|
||||
print()
|
||||
|
||||
elif args.baseline:
|
||||
print("📊 Testing BASELINE short settings...")
|
||||
print(" RSI: 30-70")
|
||||
print(" Position Min: 5%")
|
||||
print(" ADX Min: 15")
|
||||
print(" Entry Buffer: -0.15")
|
||||
print()
|
||||
|
||||
inputs = ShortOnlyInputs() # Default settings
|
||||
config = TradeConfig(tp1_percent=1.45, tp1_size=100, sl_percent=2.0)
|
||||
|
||||
result = simulate_shorts_only(df, "SOLUSDT", inputs, config)
|
||||
|
||||
print("=" * 60)
|
||||
print(" BASELINE RESULTS")
|
||||
print("=" * 60)
|
||||
print(f" Total Trades: {result.total_trades}")
|
||||
print(f" Win Rate: {result.win_rate*100:.1f}%")
|
||||
print(f" Total PnL: ${result.total_pnl:.2f}")
|
||||
print(f" Avg PnL: ${result.average_pnl:.2f}")
|
||||
print(f" Profit Factor: {result.profit_factor:.3f}")
|
||||
print(f" Max Drawdown: ${result.max_drawdown:.2f}")
|
||||
|
||||
elif args.custom:
|
||||
print("📊 Testing CUSTOM short settings...")
|
||||
print(f" RSI: {args.rsi_min}-{args.rsi_max}")
|
||||
print(f" Position Min: {args.pos_min}%")
|
||||
print(f" ADX Min: {args.adx_min}")
|
||||
print(f" Entry Buffer: {args.entry_buffer}")
|
||||
print()
|
||||
|
||||
inputs = ShortOnlyInputs(
|
||||
rsi_short_min=args.rsi_min,
|
||||
rsi_short_max=args.rsi_max,
|
||||
short_pos_min=args.pos_min,
|
||||
adx_min=args.adx_min,
|
||||
entry_buffer_atr=args.entry_buffer,
|
||||
)
|
||||
config = TradeConfig(tp1_percent=1.45, tp1_size=100, sl_percent=2.0)
|
||||
|
||||
result = simulate_shorts_only(df, "SOLUSDT", inputs, config)
|
||||
|
||||
print("=" * 60)
|
||||
print(" CUSTOM RESULTS")
|
||||
print("=" * 60)
|
||||
print(f" Total Trades: {result.total_trades}")
|
||||
print(f" Win Rate: {result.win_rate*100:.1f}%")
|
||||
print(f" Total PnL: ${result.total_pnl:.2f}")
|
||||
print(f" Avg PnL: ${result.average_pnl:.2f}")
|
||||
print(f" Profit Factor: {result.profit_factor:.3f}")
|
||||
print(f" Max Drawdown: ${result.max_drawdown:.2f}")
|
||||
|
||||
else:
|
||||
parser.print_help()
|
||||
print("\n❌ Please specify --sweep, --baseline, or --custom")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
500
ha-setup/dns-failover-monitor-improved.py
Normal file
500
ha-setup/dns-failover-monitor-improved.py
Normal file
@@ -0,0 +1,500 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
DNS Failover Monitor with Bot Start/Stop (IMPROVED Dec 2025)
|
||||
|
||||
CRITICAL IMPROVEMENT: Secondary trading bot stays STOPPED until failover.
|
||||
This prevents dual-bot interference where both bots trade the same wallet.
|
||||
|
||||
Failover sequence:
|
||||
1. Detect primary failure (3 consecutive checks, ~90 seconds)
|
||||
2. Create DEMOTED flag on primary (prevent split-brain)
|
||||
3. Promote secondary database to read-write
|
||||
4. Update DNS to secondary IP
|
||||
5. START secondary trading bot <-- NEW!
|
||||
|
||||
Failback sequence:
|
||||
1. Detect primary recovery
|
||||
2. STOP secondary trading bot <-- NEW!
|
||||
3. Update DNS to primary IP
|
||||
4. Manual: Reconfigure old primary as secondary
|
||||
"""
|
||||
|
||||
import requests
|
||||
import time
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from xmlrpc.client import ServerProxy
|
||||
from datetime import datetime
|
||||
|
||||
# Configuration
|
||||
PRIMARY_URL = os.getenv('PRIMARY_URL', 'https://flow.egonetix.de/api/health')
|
||||
PRIMARY_HOST = '95.216.52.28'
|
||||
SECONDARY_IP = '72.62.39.24'
|
||||
PRIMARY_IP = '95.216.52.28'
|
||||
CHECK_INTERVAL = 30 # seconds
|
||||
FAILURE_THRESHOLD = 3 # consecutive failures before failover
|
||||
RECOVERY_CHECK_INTERVAL = 300 # 5 minutes
|
||||
|
||||
# Telegram configuration
|
||||
TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', '8240234365:AAEm6hg_XOm54x8ctnwpNYreFKRAEvWU3uY')
|
||||
TELEGRAM_CHAT_ID = os.getenv('TELEGRAM_CHAT_ID', '579304651')
|
||||
|
||||
# INWX API configuration
|
||||
INWX_API_URL = 'https://api.domrobot.com/xmlrpc/'
|
||||
INWX_USERNAME = os.getenv('INWX_USERNAME')
|
||||
INWX_PASSWORD = os.getenv('INWX_PASSWORD')
|
||||
|
||||
# State management
|
||||
STATE_FILE = '/var/lib/dns-failover-state.json'
|
||||
LOG_FILE = '/var/log/dns-failover.log'
|
||||
|
||||
# Trading bot configuration (NEW!)
|
||||
TRADING_BOT_PROJECT_DIR = '/home/icke/traderv4'
|
||||
|
||||
class DNSFailoverMonitor:
|
||||
def __init__(self):
|
||||
self.consecutive_failures = 0
|
||||
self.in_failover_mode = False
|
||||
self.record_id = None
|
||||
self.load_state()
|
||||
|
||||
def log(self, message):
|
||||
"""Log message with timestamp"""
|
||||
timestamp = datetime.now().strftime('[%Y-%m-%d %H:%M:%S]')
|
||||
log_message = f"{timestamp} {message}"
|
||||
print(log_message)
|
||||
with open(LOG_FILE, 'a') as f:
|
||||
f.write(log_message + '\n')
|
||||
|
||||
def send_telegram(self, message):
|
||||
"""Send Telegram notification"""
|
||||
try:
|
||||
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
|
||||
data = {
|
||||
'chat_id': TELEGRAM_CHAT_ID,
|
||||
'text': message,
|
||||
'parse_mode': 'HTML'
|
||||
}
|
||||
requests.post(url, data=data, timeout=10)
|
||||
except Exception as e:
|
||||
self.log(f"Failed to send Telegram: {e}")
|
||||
|
||||
def load_state(self):
|
||||
"""Load state from file"""
|
||||
try:
|
||||
if os.path.exists(STATE_FILE):
|
||||
with open(STATE_FILE, 'r') as f:
|
||||
state = json.load(f)
|
||||
self.in_failover_mode = state.get('in_failover_mode', False)
|
||||
except Exception as e:
|
||||
self.log(f"Warning: Could not load state: {e}")
|
||||
|
||||
def save_state(self):
|
||||
"""Save state to file"""
|
||||
try:
|
||||
state = {
|
||||
'in_failover_mode': self.in_failover_mode,
|
||||
'last_update': datetime.now().isoformat()
|
||||
}
|
||||
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
|
||||
with open(STATE_FILE, 'w') as f:
|
||||
json.dump(state, f)
|
||||
except Exception as e:
|
||||
self.log(f"Warning: Could not save state: {e}")
|
||||
|
||||
def check_primary_health(self):
|
||||
"""Check if primary server is responding with valid trading bot response"""
|
||||
try:
|
||||
response = requests.get(PRIMARY_URL, timeout=10, verify=True)
|
||||
|
||||
# Must be 200 status
|
||||
if response.status_code != 200:
|
||||
self.log(f"✗ Primary server returned {response.status_code}")
|
||||
return False
|
||||
|
||||
# Must have content
|
||||
if not response.text or len(response.text.strip()) == 0:
|
||||
self.log(f"✗ Primary server returned empty response")
|
||||
return False
|
||||
|
||||
# Try to parse as JSON and check for trading bot response
|
||||
try:
|
||||
data = response.json()
|
||||
# Trading bot health endpoint returns {"status": "healthy", "timestamp": ..., "uptime": ...}
|
||||
if 'status' in data and data.get('status') == 'healthy':
|
||||
self.log("✓ Primary server healthy (trading bot responding)")
|
||||
return True
|
||||
else:
|
||||
self.log(f"✗ Primary server JSON invalid: {data}")
|
||||
return False
|
||||
except json.JSONDecodeError:
|
||||
# Not JSON - likely n8n HTML or other service
|
||||
if '<html' in response.text.lower() or '<!doctype' in response.text.lower():
|
||||
self.log("✗ Primary server returned HTML (not trading bot)")
|
||||
return False
|
||||
# Unknown response
|
||||
self.log("✗ Primary server returned non-JSON response")
|
||||
return False
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.log(f"✗ Primary server check failed: {e}")
|
||||
return False
|
||||
|
||||
def get_dns_record_id(self):
|
||||
"""Get the DNS record ID for tradervone.v4.dedyn.io"""
|
||||
try:
|
||||
api = ServerProxy(INWX_API_URL)
|
||||
result = api.nameserver.info({
|
||||
'user': INWX_USERNAME,
|
||||
'pass': INWX_PASSWORD,
|
||||
'domain': 'dedyn.io',
|
||||
'name': 'tradervone.v4.dedyn.io'
|
||||
})
|
||||
|
||||
if result['code'] == 1000 and 'record' in result['resData']:
|
||||
record = result['resData']['record'][0]
|
||||
self.record_id = record['id']
|
||||
current_ip = record['content']
|
||||
self.log(f"DNS record ID: {self.record_id}, current IP: {current_ip}")
|
||||
return self.record_id, current_ip
|
||||
else:
|
||||
self.log(f"Failed to get DNS record: {result}")
|
||||
return None, None
|
||||
except Exception as e:
|
||||
self.log(f"Error getting DNS record: {e}")
|
||||
return None, None
|
||||
|
||||
def update_dns_record(self, target_ip, ttl=300):
|
||||
"""Update DNS record to point to target IP"""
|
||||
try:
|
||||
record_id, current_ip = self.get_dns_record_id()
|
||||
|
||||
if not record_id:
|
||||
self.log("Cannot update DNS: no record ID")
|
||||
return False
|
||||
|
||||
if current_ip == target_ip:
|
||||
self.log(f"DNS already points to {target_ip}, skipping update")
|
||||
return True
|
||||
|
||||
api = ServerProxy(INWX_API_URL)
|
||||
result = api.nameserver.updateRecord({
|
||||
'user': INWX_USERNAME,
|
||||
'pass': INWX_PASSWORD,
|
||||
'id': record_id,
|
||||
'content': target_ip,
|
||||
'ttl': ttl
|
||||
})
|
||||
|
||||
if result['code'] == 1000:
|
||||
self.log(f"✓ DNS updated to {target_ip} (TTL: {ttl})")
|
||||
return True
|
||||
else:
|
||||
self.log(f"Failed to update DNS: {result}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.log(f"Error updating DNS: {e}")
|
||||
return False
|
||||
|
||||
def promote_secondary_database(self):
|
||||
"""Promote secondary PostgreSQL database to primary"""
|
||||
try:
|
||||
self.log("🔄 Promoting secondary database to primary (read-write mode)...")
|
||||
|
||||
# Run pg_ctl promote command
|
||||
cmd = [
|
||||
'docker', 'exec', 'trading-bot-postgres',
|
||||
'/usr/lib/postgresql/16/bin/pg_ctl', 'promote',
|
||||
'-D', '/var/lib/postgresql/data'
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
|
||||
if result.returncode == 0:
|
||||
self.log("✅ Database promoted to primary successfully")
|
||||
|
||||
# Verify it's writable
|
||||
time.sleep(2)
|
||||
verify_cmd = [
|
||||
'docker', 'exec', 'trading-bot-postgres',
|
||||
'psql', '-U', 'postgres', '-c', 'SELECT pg_is_in_recovery();'
|
||||
]
|
||||
verify = subprocess.run(verify_cmd, capture_output=True, text=True, timeout=10)
|
||||
|
||||
if 'f' in verify.stdout: # f = false = not in recovery = primary
|
||||
self.log("✅ Database is now PRIMARY (writable)")
|
||||
return True
|
||||
else:
|
||||
self.log("⚠️ Database promotion uncertain")
|
||||
return False
|
||||
else:
|
||||
self.log(f"❌ Database promotion failed: {result.stderr}")
|
||||
return False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
self.log("❌ Database promotion timed out")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.log(f"❌ Error promoting database: {e}")
|
||||
return False
|
||||
|
||||
def create_demoted_flag_on_primary(self):
|
||||
"""Create DEMOTED flag file on old primary to prevent split-brain"""
|
||||
try:
|
||||
self.log("🚫 Creating DEMOTED flag on old primary...")
|
||||
|
||||
# SSH to primary and create flag in database data directory
|
||||
cmd = [
|
||||
'ssh', '-o', 'ConnectTimeout=5', f'root@{PRIMARY_HOST}',
|
||||
'docker', 'exec', 'trading-bot-postgres',
|
||||
'touch', '/var/lib/postgresql/data/DEMOTED'
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||
|
||||
if result.returncode == 0:
|
||||
self.log("✅ DEMOTED flag created on primary")
|
||||
return True
|
||||
else:
|
||||
self.log(f"⚠️ Could not create flag (primary may be down): {result.stderr}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"⚠️ Error creating DEMOTED flag: {e}")
|
||||
return False
|
||||
|
||||
# ==========================================
|
||||
# NEW: Trading Bot Control Functions
|
||||
# ==========================================
|
||||
|
||||
def start_secondary_trading_bot(self):
|
||||
"""Start the trading bot on secondary server (this server)
|
||||
|
||||
CRITICAL: This is called AFTER DNS switch and database promotion.
|
||||
The bot starts and immediately begins monitoring/trading.
|
||||
"""
|
||||
try:
|
||||
self.log("🚀 Starting secondary trading bot...")
|
||||
|
||||
# Use docker compose start (not up) since container already exists but is stopped
|
||||
cmd = [
|
||||
'docker', 'compose', '-f', f'{TRADING_BOT_PROJECT_DIR}/docker-compose.yml',
|
||||
'start', 'trading-bot'
|
||||
]
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
cwd=TRADING_BOT_PROJECT_DIR
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
self.log("✅ Secondary trading bot STARTED successfully")
|
||||
|
||||
# Wait a moment and verify it's running
|
||||
time.sleep(5)
|
||||
verify_cmd = ['docker', 'ps', '--filter', 'name=trading-bot-v4', '--format', '{{.Status}}']
|
||||
verify = subprocess.run(verify_cmd, capture_output=True, text=True, timeout=10)
|
||||
|
||||
if 'Up' in verify.stdout:
|
||||
self.log("✅ Trading bot is running and healthy")
|
||||
return True
|
||||
else:
|
||||
self.log(f"⚠️ Trading bot status uncertain: {verify.stdout}")
|
||||
return False
|
||||
else:
|
||||
self.log(f"❌ Failed to start trading bot: {result.stderr}")
|
||||
return False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
self.log("❌ Trading bot start timed out")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.log(f"❌ Error starting trading bot: {e}")
|
||||
return False
|
||||
|
||||
def stop_secondary_trading_bot(self):
|
||||
"""Stop the trading bot on secondary server (this server)
|
||||
|
||||
CRITICAL: This is called during failback to prevent dual-bot operation.
|
||||
Primary is recovered, so secondary bot must STOP.
|
||||
"""
|
||||
try:
|
||||
self.log("🛑 Stopping secondary trading bot...")
|
||||
|
||||
cmd = [
|
||||
'docker', 'compose', '-f', f'{TRADING_BOT_PROJECT_DIR}/docker-compose.yml',
|
||||
'stop', 'trading-bot'
|
||||
]
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
cwd=TRADING_BOT_PROJECT_DIR
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
self.log("✅ Secondary trading bot STOPPED successfully")
|
||||
return True
|
||||
else:
|
||||
self.log(f"❌ Failed to stop trading bot: {result.stderr}")
|
||||
return False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
self.log("❌ Trading bot stop timed out")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.log(f"❌ Error stopping trading bot: {e}")
|
||||
return False
|
||||
|
||||
def check_trading_bot_status(self):
|
||||
"""Check if trading bot is currently running"""
|
||||
try:
|
||||
cmd = ['docker', 'ps', '--filter', 'name=trading-bot-v4', '--format', '{{.Status}}']
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||
return 'Up' in result.stdout
|
||||
except:
|
||||
return False
|
||||
|
||||
# ==========================================
|
||||
# Failover/Failback with Bot Control
|
||||
# ==========================================
|
||||
|
||||
def failover_to_secondary(self):
|
||||
"""Switch DNS to secondary server, promote database, and START bot"""
|
||||
self.log("🚨 INITIATING AUTOMATIC FAILOVER TO SECONDARY")
|
||||
|
||||
# Step 1: Try to create DEMOTED flag on old primary (may fail if down)
|
||||
self.create_demoted_flag_on_primary()
|
||||
|
||||
# Step 2: Promote secondary database to primary
|
||||
db_promoted = self.promote_secondary_database()
|
||||
|
||||
# Step 3: Update DNS
|
||||
dns_updated = self.update_dns_record(SECONDARY_IP, ttl=300)
|
||||
|
||||
# Step 4: START the trading bot (NEW!)
|
||||
bot_started = False
|
||||
if dns_updated:
|
||||
bot_started = self.start_secondary_trading_bot()
|
||||
|
||||
if dns_updated:
|
||||
self.in_failover_mode = True
|
||||
self.consecutive_failures = 0
|
||||
self.save_state()
|
||||
|
||||
# Determine overall status
|
||||
if db_promoted and bot_started:
|
||||
status = "✅ COMPLETE"
|
||||
elif bot_started:
|
||||
status = "⚠️ PARTIAL (DB check needed)"
|
||||
else:
|
||||
status = "❌ CRITICAL (Bot not started!)"
|
||||
|
||||
self.log(f"{status} Failover to secondary")
|
||||
|
||||
# Send Telegram notification
|
||||
telegram_msg = f"🚨 <b>AUTOMATIC FAILOVER ACTIVATED</b>\n\n"
|
||||
telegram_msg += f"Primary: {PRIMARY_IP} → FAILED\n"
|
||||
telegram_msg += f"Secondary: {SECONDARY_IP} → NOW PRIMARY\n\n"
|
||||
telegram_msg += f"Database: {'✅ Promoted (writable)' if db_promoted else '⚠️ Check manually'}\n"
|
||||
telegram_msg += f"DNS: ✅ Updated\n"
|
||||
telegram_msg += f"<b>Trading Bot: {'✅ STARTED' if bot_started else '❌ FAILED TO START!'}</b>\n\n"
|
||||
telegram_msg += f"Status: {status}\n"
|
||||
telegram_msg += f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
|
||||
if not bot_started:
|
||||
telegram_msg += f"\n\n⚠️ <b>URGENT:</b> Trading bot failed to start!\n"
|
||||
telegram_msg += f"Manual action: cd {TRADING_BOT_PROJECT_DIR} && docker compose start trading-bot"
|
||||
|
||||
self.send_telegram(telegram_msg)
|
||||
else:
|
||||
self.log("❌ Failover FAILED - DNS update unsuccessful")
|
||||
|
||||
def failback_to_primary(self):
|
||||
"""Stop secondary bot, switch DNS back to primary server"""
|
||||
self.log("🔄 INITIATING FAILBACK TO PRIMARY")
|
||||
|
||||
# Step 1: STOP the secondary trading bot FIRST (NEW!)
|
||||
bot_stopped = self.stop_secondary_trading_bot()
|
||||
|
||||
# Step 2: Update DNS back to primary
|
||||
dns_updated = self.update_dns_record(PRIMARY_IP, ttl=3600)
|
||||
|
||||
if dns_updated:
|
||||
self.in_failover_mode = False
|
||||
self.consecutive_failures = 0
|
||||
self.save_state()
|
||||
self.log("✓ Failback complete - now using PRIMARY server")
|
||||
|
||||
# Send Telegram notification
|
||||
telegram_msg = f"🔄 <b>AUTOMATIC FAILBACK COMPLETE</b>\n\n"
|
||||
telegram_msg += f"Primary: {PRIMARY_IP} → RECOVERED\n"
|
||||
telegram_msg += f"Secondary: {SECONDARY_IP} → STANDBY\n\n"
|
||||
telegram_msg += f"DNS: ✅ Switched to primary\n"
|
||||
telegram_msg += f"<b>Secondary Bot: {'✅ STOPPED' if bot_stopped else '⚠️ May still be running!'}</b>\n"
|
||||
telegram_msg += f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
telegram_msg += f"⚠️ <b>Manual Action Required:</b>\n"
|
||||
telegram_msg += f"1. Verify primary bot is healthy\n"
|
||||
telegram_msg += f"2. Reconfigure secondary database as replica"
|
||||
|
||||
if not bot_stopped:
|
||||
telegram_msg += f"\n\n❌ <b>WARNING:</b> Secondary bot may still be running!\n"
|
||||
telegram_msg += f"Stop manually: cd {TRADING_BOT_PROJECT_DIR} && docker compose stop trading-bot"
|
||||
|
||||
self.send_telegram(telegram_msg)
|
||||
else:
|
||||
self.log("✗ Failback failed - DNS update unsuccessful")
|
||||
|
||||
def run(self):
|
||||
"""Main monitoring loop"""
|
||||
self.log("=== DNS Failover Monitor Starting (IMPROVED with Bot Start/Stop) ===")
|
||||
self.log(f"Primary URL: {PRIMARY_URL}")
|
||||
self.log(f"Check interval: {CHECK_INTERVAL}s")
|
||||
self.log(f"Failure threshold: {FAILURE_THRESHOLD}")
|
||||
self.log(f"Trading bot project: {TRADING_BOT_PROJECT_DIR}")
|
||||
|
||||
# Get initial DNS state
|
||||
self.get_dns_record_id()
|
||||
|
||||
# Check if trading bot is currently running (should be STOPPED on secondary in standby)
|
||||
bot_running = self.check_trading_bot_status()
|
||||
if bot_running and not self.in_failover_mode:
|
||||
self.log("⚠️ WARNING: Secondary bot is running but we're in standby mode!")
|
||||
self.log("⚠️ This may cause dual-bot interference. Consider stopping secondary bot.")
|
||||
|
||||
while True:
|
||||
try:
|
||||
if self.in_failover_mode:
|
||||
# In failover mode - check if primary recovered
|
||||
if self.check_primary_health():
|
||||
self.log("Primary server recovered!")
|
||||
self.failback_to_primary()
|
||||
time.sleep(RECOVERY_CHECK_INTERVAL)
|
||||
else:
|
||||
# Normal mode - monitor primary
|
||||
if self.check_primary_health():
|
||||
self.consecutive_failures = 0
|
||||
else:
|
||||
self.consecutive_failures += 1
|
||||
self.log(f"Failure count: {self.consecutive_failures}/{FAILURE_THRESHOLD}")
|
||||
|
||||
if self.consecutive_failures >= FAILURE_THRESHOLD:
|
||||
self.failover_to_secondary()
|
||||
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
except KeyboardInterrupt:
|
||||
self.log("Monitor stopped by user")
|
||||
break
|
||||
except Exception as e:
|
||||
self.log(f"Error in main loop: {e}")
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
|
||||
if __name__ == '__main__':
|
||||
monitor = DNSFailoverMonitor()
|
||||
monitor.run()
|
||||
@@ -172,7 +172,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Get data from previous nodes\nconst resp = $('Execute Trade1').item.json;\nconst parsed = $('Parse Signal Enhanced').item.json;\n\nlet message = '';\n\n// Smart Entry Queue (no entry yet, just queued)\nif (resp.smartEntry && !resp.entryPrice) {\n const emoji = parsed.direction === 'long' ? '\ud83d\udcc8' : '\ud83d\udcc9';\n message = `\u23f0 SIGNAL QUEUED FOR SMART ENTRY\n\n${emoji} Direction: ${parsed.direction.toUpperCase()}\n\n Entry window: 5 minutes\n\n Quality: ${resp.qualityScore || 'N/A'}/100\n\n${$now.setZone('Europe/Berlin').toFormat('HH:mm:ss')}\n}\n// Trade Opened (has entry price)\nelse if (resp.entryPrice) {\n const emoji = parsed.direction === 'long' ? '\ud83d\udcc8' : '\ud83d\udcc9';\n const qualityLine = resp.qualityScore ? `\\n\\n\u2b50 Quality: ${resp.qualityScore}/100` : '';\n \n message = `\ud83d\udfe2 TRADE OPENED\n\n${emoji} Direction: ${parsed.direction.toUpperCase()}\n\n Leverage: ${resp.leverage}x${qualityLine}\n\n\n${$now.setZone('Europe/Berlin').toFormat('HH:mm:ss')}\n Position monitored`;\n}\n// Fallback: Signal processed but no entry\nelse {\n const emoji = parsed.direction === 'long' ? '\ud83d\udcc8' : '\ud83d\udcc9';\n message = `\ud83d\udcdd SIGNAL PROCESSED\n\n${emoji} Direction: ${parsed.direction.toUpperCase()}\n\n ${resp.message || 'Response received'}\n\n${$now.setZone('Europe/Berlin').toFormat('HH:mm:ss')}`;\n}\n\nreturn { message };"
|
||||
"jsCode": "// Get data from previous nodes\nconst resp = $('Execute Trade1').item.json;\nconst parsed = $('Parse Signal Enhanced').item.json;\n\nlet message = '';\n\n// Helper to format price\nconst formatPrice = (price) => price ? `$${Number(price).toFixed(2)}` : 'N/A';\n\n// Smart Entry Queue (no entry yet, just queued)\nif (resp.smartEntry && !resp.entryPrice) {\n const emoji = parsed.direction === 'long' ? '\ud83d\udcc8' : '\ud83d\udcc9';\n message = `\u23f0 SIGNAL QUEUED FOR SMART ENTRY\n\n${emoji} Direction: ${parsed.direction.toUpperCase()}\n\n Entry window: 5 minutes\n\n Quality: ${resp.qualityScore || 'N/A'}/100\n\n${$now.setZone('Europe/Berlin').toFormat('HH:mm:ss')}`;\n}\n// Trade Opened (has entry price)\nelse if (resp.entryPrice) {\n const emoji = parsed.direction === 'long' ? '\ud83d\udcc8' : '\ud83d\udcc9';\n const qualityLine = resp.qualityScore ? `\\n\u2b50 Quality: ${resp.qualityScore}/100` : '';\n const tp1Line = `\\n\ud83c\udfaf TP1: ${formatPrice(resp.takeProfit1)}`;\n const tp2Line = `\\n\ud83c\udfaf TP2: ${formatPrice(resp.takeProfit2)}`;\n const slLine = `\\n\ud83d\uded1 SL: ${formatPrice(resp.stopLoss)}`;\n \n message = `\ud83d\udfe2 TRADE OPENED\n\n${emoji} ${parsed.direction.toUpperCase()} @ ${formatPrice(resp.entryPrice)}\n\ud83d\udcb0 Size: $${resp.positionSize?.toFixed(0) || 'N/A'} (${resp.leverage}x)${qualityLine}${tp1Line}${tp2Line}${slLine}\n\n\u23f0 ${$now.setZone('Europe/Berlin').toFormat('HH:mm:ss')}\n\u2705 Position monitored`;\n}\n// Fallback: Signal processed but no entry\nelse {\n const emoji = parsed.direction === 'long' ? '\ud83d\udcc8' : '\ud83d\udcc9';\n message = `\ud83d\udcdd SIGNAL PROCESSED\n\n${emoji} Direction: ${parsed.direction.toUpperCase()}\n\n ${resp.message || 'Response received'}\n\n${$now.setZone('Europe/Berlin').toFormat('HH:mm:ss')}`;\n}\n\nreturn { message };"
|
||||
},
|
||||
"id": "79ab6122-cbd3-4aac-97d7-6b54f64e29b5",
|
||||
"name": "Format Success",
|
||||
|
||||
194
workflows/trading/mean_reversion_shorts_strategy.pinescript
Normal file
194
workflows/trading/mean_reversion_shorts_strategy.pinescript
Normal file
@@ -0,0 +1,194 @@
|
||||
//@version=6
|
||||
strategy("Mean Reversion SHORTS", shorttitle="MR Shorts", overlay=true,
|
||||
default_qty_type=strategy.percent_of_equity, default_qty_value=100,
|
||||
initial_capital=1000, commission_type=strategy.commission.percent, commission_value=0.05)
|
||||
|
||||
// =============================================================================
|
||||
// MEAN REVERSION SHORTS STRATEGY (Jan 6, 2026)
|
||||
// =============================================================================
|
||||
// PURPOSE: Catch overbought exhaustion for SHORT entries
|
||||
//
|
||||
// CONCEPT: Instead of trend-following (Money Line), this strategy:
|
||||
// - Waits for RSI overbought (70+)
|
||||
// - Confirms with price at top of range (80%+)
|
||||
// - Looks for volume exhaustion or spike
|
||||
// - Optional: Bollinger Band upper touch
|
||||
// - Optional: Stochastic overbought cross down
|
||||
//
|
||||
// This is CONTRARIAN - shorting tops, not following trends down
|
||||
// =============================================================================
|
||||
|
||||
// === RSI SETTINGS ===
|
||||
rsiLen = input.int(14, "RSI Length", minval=5, maxval=30, group="RSI")
|
||||
rsiOverbought = input.float(70, "RSI Overbought Level", minval=60, maxval=90, group="RSI")
|
||||
rsiExitLevel = input.float(50, "RSI Exit Level", minval=30, maxval=60, group="RSI")
|
||||
|
||||
// === PRICE POSITION ===
|
||||
pricePosPeriod = input.int(100, "Price Position Period", minval=20, maxval=200, group="Price Position")
|
||||
pricePosMin = input.float(75, "Min Price Position %", minval=50, maxval=95, group="Price Position")
|
||||
|
||||
// === BOLLINGER BANDS (optional confirmation) ===
|
||||
useBB = input.bool(true, "Use Bollinger Band Filter", group="Bollinger Bands")
|
||||
bbLen = input.int(20, "BB Length", minval=10, maxval=50, group="Bollinger Bands")
|
||||
bbMult = input.float(2.0, "BB Multiplier", minval=1.0, maxval=3.0, step=0.1, group="Bollinger Bands")
|
||||
|
||||
// === STOCHASTIC (optional confirmation) ===
|
||||
useStoch = input.bool(false, "Use Stochastic Filter", group="Stochastic")
|
||||
stochK = input.int(14, "Stoch K", minval=5, maxval=30, group="Stochastic")
|
||||
stochD = input.int(3, "Stoch D", minval=1, maxval=10, group="Stochastic")
|
||||
stochOB = input.float(80, "Stoch Overbought", minval=70, maxval=95, group="Stochastic")
|
||||
|
||||
// === VOLUME FILTER ===
|
||||
useVolume = input.bool(true, "Use Volume Filter", group="Volume")
|
||||
volPeriod = input.int(20, "Volume MA Period", minval=10, maxval=50, group="Volume")
|
||||
volMultMin = input.float(1.2, "Min Volume Multiple", minval=0.5, maxval=3.0, step=0.1, group="Volume")
|
||||
|
||||
// === ADX FILTER (optional - avoid strong trends) ===
|
||||
useADX = input.bool(true, "Use ADX Filter (avoid strong trends)", group="ADX")
|
||||
adxLen = input.int(14, "ADX Length", minval=7, maxval=30, group="ADX")
|
||||
adxMax = input.float(30, "ADX Maximum (weaker = better for MR)", minval=15, maxval=50, group="ADX")
|
||||
|
||||
// === CONSECUTIVE CANDLES ===
|
||||
useConsec = input.bool(true, "Require Consecutive Green Candles", group="Candle Pattern")
|
||||
consecMin = input.int(3, "Min Consecutive Green Candles", minval=2, maxval=7, group="Candle Pattern")
|
||||
|
||||
// === EXIT SETTINGS ===
|
||||
tpPercent = input.float(1.5, "Take Profit %", minval=0.5, maxval=5.0, step=0.1, group="Exit")
|
||||
slPercent = input.float(2.0, "Stop Loss %", minval=0.5, maxval=5.0, step=0.1, group="Exit")
|
||||
useRsiExit = input.bool(true, "Exit on RSI Mean Reversion", group="Exit")
|
||||
|
||||
// =============================================================================
|
||||
// INDICATORS
|
||||
// =============================================================================
|
||||
|
||||
// RSI
|
||||
rsi = ta.rsi(close, rsiLen)
|
||||
|
||||
// Price Position (where is price in recent range)
|
||||
highest = ta.highest(high, pricePosPeriod)
|
||||
lowest = ta.lowest(low, pricePosPeriod)
|
||||
priceRange = highest - lowest
|
||||
pricePosition = priceRange == 0 ? 50 : ((close - lowest) / priceRange) * 100
|
||||
|
||||
// Bollinger Bands
|
||||
bbBasis = ta.sma(close, bbLen)
|
||||
bbDev = bbMult * ta.stdev(close, bbLen)
|
||||
bbUpper = bbBasis + bbDev
|
||||
bbLower = bbBasis - bbDev
|
||||
nearUpperBB = close >= bbUpper * 0.995 // Within 0.5% of upper band
|
||||
|
||||
// Stochastic
|
||||
stochKVal = ta.stoch(close, high, low, stochK)
|
||||
stochDVal = ta.sma(stochKVal, stochD)
|
||||
stochOverbought = stochKVal > stochOB and stochDVal > stochOB
|
||||
stochCrossDown = ta.crossunder(stochKVal, stochDVal) and stochKVal[1] > stochOB
|
||||
|
||||
// Volume
|
||||
volMA = ta.sma(volume, volPeriod)
|
||||
volRatio = volume / volMA
|
||||
volumeSpike = volRatio >= volMultMin
|
||||
|
||||
// ADX (for filtering out strong trends)
|
||||
[diPlus, diMinus, adxVal] = ta.dmi(adxLen, adxLen)
|
||||
weakTrend = adxVal < adxMax
|
||||
|
||||
// Consecutive green candles (exhaustion setup)
|
||||
isGreen = close > open
|
||||
greenCount = ta.barssince(not isGreen)
|
||||
hasConsecGreen = greenCount >= consecMin
|
||||
|
||||
// =============================================================================
|
||||
// ENTRY CONDITIONS
|
||||
// =============================================================================
|
||||
|
||||
// Core conditions (always required)
|
||||
rsiOB = rsi >= rsiOverbought
|
||||
priceAtTop = pricePosition >= pricePosMin
|
||||
|
||||
// Optional confirmations
|
||||
bbOk = not useBB or nearUpperBB
|
||||
stochOk = not useStoch or stochOverbought or stochCrossDown
|
||||
volumeOk = not useVolume or volumeSpike
|
||||
adxOk = not useADX or weakTrend
|
||||
consecOk = not useConsec or hasConsecGreen
|
||||
|
||||
// FINAL SHORT SIGNAL
|
||||
shortSignal = rsiOB and priceAtTop and bbOk and stochOk and volumeOk and adxOk and consecOk
|
||||
|
||||
// =============================================================================
|
||||
// STRATEGY EXECUTION
|
||||
// =============================================================================
|
||||
|
||||
// Entry
|
||||
if shortSignal and strategy.position_size == 0
|
||||
strategy.entry("Short", strategy.short)
|
||||
|
||||
// Exit with TP/SL
|
||||
if strategy.position_size < 0
|
||||
entryPrice = strategy.position_avg_price
|
||||
tpPrice = entryPrice * (1 - tpPercent / 100)
|
||||
slPrice = entryPrice * (1 + slPercent / 100)
|
||||
strategy.exit("Exit", "Short", limit=tpPrice, stop=slPrice)
|
||||
|
||||
// RSI mean reversion exit (optional)
|
||||
if useRsiExit and strategy.position_size < 0 and rsi <= rsiExitLevel
|
||||
strategy.close("Short", comment="RSI Exit")
|
||||
|
||||
// =============================================================================
|
||||
// PLOTS
|
||||
// =============================================================================
|
||||
|
||||
// Bollinger Bands
|
||||
plot(useBB ? bbUpper : na, "BB Upper", color=color.new(color.red, 50), linewidth=1)
|
||||
plot(useBB ? bbBasis : na, "BB Mid", color=color.new(color.gray, 50), linewidth=1)
|
||||
plot(useBB ? bbLower : na, "BB Lower", color=color.new(color.green, 50), linewidth=1)
|
||||
|
||||
// Entry signals
|
||||
plotshape(shortSignal, title="Short Signal", location=location.abovebar,
|
||||
color=color.red, style=shape.triangledown, size=size.small)
|
||||
|
||||
// Background when overbought
|
||||
bgcolor(rsi >= rsiOverbought ? color.new(color.red, 90) : na)
|
||||
|
||||
// =============================================================================
|
||||
// DEBUG TABLE
|
||||
// =============================================================================
|
||||
|
||||
var table dbg = table.new(position.top_right, 2, 10, bgcolor=color.new(color.black, 80))
|
||||
if barstate.islast
|
||||
table.cell(dbg, 0, 0, "STRATEGY", text_color=color.white)
|
||||
table.cell(dbg, 1, 0, "MEAN REVERSION", text_color=color.orange)
|
||||
|
||||
table.cell(dbg, 0, 1, "RSI", text_color=color.white)
|
||||
table.cell(dbg, 1, 1, str.tostring(rsi, "#.#") + " >= " + str.tostring(rsiOverbought) + (rsiOB ? " ✓" : " ✗"),
|
||||
text_color=rsiOB ? color.lime : color.red)
|
||||
|
||||
table.cell(dbg, 0, 2, "Price Pos", text_color=color.white)
|
||||
table.cell(dbg, 1, 2, str.tostring(pricePosition, "#.#") + "% >= " + str.tostring(pricePosMin) + "%" + (priceAtTop ? " ✓" : " ✗"),
|
||||
text_color=priceAtTop ? color.lime : color.red)
|
||||
|
||||
table.cell(dbg, 0, 3, "BB Upper", text_color=color.white)
|
||||
table.cell(dbg, 1, 3, useBB ? (nearUpperBB ? "TOUCH ✓" : "—") : "OFF",
|
||||
text_color=nearUpperBB ? color.lime : color.gray)
|
||||
|
||||
table.cell(dbg, 0, 4, "Volume", text_color=color.white)
|
||||
table.cell(dbg, 1, 4, useVolume ? (str.tostring(volRatio, "#.#") + "x" + (volumeOk ? " ✓" : " ✗")) : "OFF",
|
||||
text_color=volumeOk ? color.lime : color.gray)
|
||||
|
||||
table.cell(dbg, 0, 5, "ADX", text_color=color.white)
|
||||
table.cell(dbg, 1, 5, useADX ? (str.tostring(adxVal, "#.#") + " < " + str.tostring(adxMax) + (weakTrend ? " ✓" : " ✗")) : "OFF",
|
||||
text_color=weakTrend ? color.lime : color.red)
|
||||
|
||||
table.cell(dbg, 0, 6, "Consec Green", text_color=color.white)
|
||||
table.cell(dbg, 1, 6, useConsec ? (str.tostring(greenCount) + " >= " + str.tostring(consecMin) + (hasConsecGreen ? " ✓" : " ✗")) : "OFF",
|
||||
text_color=hasConsecGreen ? color.lime : color.gray)
|
||||
|
||||
table.cell(dbg, 0, 7, "Stochastic", text_color=color.white)
|
||||
table.cell(dbg, 1, 7, useStoch ? (str.tostring(stochKVal, "#.#") + (stochOk ? " ✓" : " ✗")) : "OFF",
|
||||
text_color=stochOk ? color.lime : color.gray)
|
||||
|
||||
table.cell(dbg, 0, 8, "TP/SL", text_color=color.white)
|
||||
table.cell(dbg, 1, 8, str.tostring(tpPercent, "#.#") + "% / " + str.tostring(slPercent, "#.#") + "%", text_color=color.yellow)
|
||||
|
||||
table.cell(dbg, 0, 9, "SIGNAL", text_color=color.white)
|
||||
table.cell(dbg, 1, 9, shortSignal ? "SHORT!" : "—", text_color=shortSignal ? color.red : color.gray)
|
||||
203
workflows/trading/moneyline_v11_2_shorts_strategy.pinescript
Normal file
203
workflows/trading/moneyline_v11_2_shorts_strategy.pinescript
Normal file
@@ -0,0 +1,203 @@
|
||||
//@version=6
|
||||
strategy("Money Line v11.2 SHORTS STRATEGY", shorttitle="ML Shorts", overlay=true,
|
||||
default_qty_type=strategy.percent_of_equity, default_qty_value=100,
|
||||
initial_capital=1000, commission_type=strategy.commission.percent, commission_value=0.05)
|
||||
|
||||
// =============================================================================
|
||||
// V11.2 SHORTS-ONLY STRATEGY (Jan 6, 2026)
|
||||
// =============================================================================
|
||||
// PURPOSE: Backtest and optimize SHORT parameters separately from LONGs
|
||||
//
|
||||
// FIXED PARAMETERS (from proven v11.2opt - DO NOT CHANGE):
|
||||
// - ATR Period: 12, Multiplier: 3.8
|
||||
// - ADX Length: 17
|
||||
// - Flip Threshold: 0.0%, Confirm Bars: 1
|
||||
//
|
||||
// SWEEP PARAMETERS FOR SHORTS (what you're optimizing):
|
||||
// - RSI Short Min/Max (default 30-70, try 30-50, 25-45, etc.)
|
||||
// - Short Position Min (default 5%, try 15%, 25%, 35%)
|
||||
// - ADX Minimum (default 15, try 18, 20, 22)
|
||||
// - Entry Buffer ATR (default -0.15, try 0.0, 0.1, 0.15)
|
||||
//
|
||||
// TP/SL SETTINGS (match production):
|
||||
// - TP1: 1.45% (100% close)
|
||||
// - SL: 2.0%
|
||||
// =============================================================================
|
||||
|
||||
// === FIXED CORE PARAMETERS (from v11.2opt - DO NOT MODIFY) ===
|
||||
atrPeriod = 12
|
||||
multiplier = 3.8
|
||||
adxLen = 17
|
||||
confirmBars = 1
|
||||
flipThreshold = 0.0
|
||||
|
||||
// === SHORT SWEEP PARAMETERS (OPTIMIZE THESE) ===
|
||||
adxMin = input.int(15, "ADX Minimum", minval=5, maxval=35, group="SHORT Filters")
|
||||
rsiShortMin = input.float(30, "RSI Short Min", minval=0, maxval=100, group="SHORT Filters")
|
||||
rsiShortMax = input.float(70, "RSI Short Max", minval=0, maxval=100, group="SHORT Filters")
|
||||
shortPosMin = input.float(5, "Short Position Min %", minval=0, maxval=100, group="SHORT Filters")
|
||||
entryBufferATR = input.float(-0.15, "Entry Buffer (ATR)", minval=-1.0, maxval=1.0, step=0.05, group="SHORT Filters")
|
||||
|
||||
// === VOLUME FILTER (optional) ===
|
||||
useVolumeFilter = input.bool(false, "Use Volume Filter", group="Volume")
|
||||
volMin = input.float(0.1, "Volume Min Ratio", minval=0.0, step=0.1, group="Volume")
|
||||
volMax = input.float(3.5, "Volume Max Ratio", minval=0.5, step=0.5, group="Volume")
|
||||
|
||||
// === TP/SL SETTINGS (match production) ===
|
||||
tpPercent = input.float(1.45, "Take Profit %", minval=0.1, maxval=10.0, step=0.05, group="Exit")
|
||||
slPercent = input.float(2.0, "Stop Loss %", minval=0.1, maxval=10.0, step=0.1, group="Exit")
|
||||
|
||||
// =============================================================================
|
||||
// MONEY LINE CALCULATION (identical to indicator)
|
||||
// =============================================================================
|
||||
|
||||
calcH = high
|
||||
calcL = low
|
||||
calcC = close
|
||||
|
||||
// ATR
|
||||
tr = math.max(calcH - calcL, math.max(math.abs(calcH - calcC[1]), math.abs(calcL - calcC[1])))
|
||||
atr = ta.rma(tr, atrPeriod)
|
||||
src = (calcH + calcL) / 2
|
||||
|
||||
up = src - (multiplier * atr)
|
||||
dn = src + (multiplier * atr)
|
||||
|
||||
var float up1 = na
|
||||
var float dn1 = na
|
||||
|
||||
up1 := nz(up1[1], up)
|
||||
dn1 := nz(dn1[1], dn)
|
||||
|
||||
up1 := calcC[1] > up1 ? math.max(up, up1) : up
|
||||
dn1 := calcC[1] < dn1 ? math.min(dn, dn1) : dn
|
||||
|
||||
var int trend = 1
|
||||
var float tsl = na
|
||||
|
||||
tsl := nz(tsl[1], up1)
|
||||
|
||||
thresholdAmount = tsl * (flipThreshold / 100)
|
||||
|
||||
var int bullMomentumBars = 0
|
||||
var int bearMomentumBars = 0
|
||||
|
||||
if trend == 1
|
||||
tsl := math.max(up1, tsl)
|
||||
if calcC < (tsl - thresholdAmount)
|
||||
bearMomentumBars := bearMomentumBars + 1
|
||||
bullMomentumBars := 0
|
||||
else
|
||||
bearMomentumBars := 0
|
||||
trend := bearMomentumBars >= (confirmBars + 1) ? -1 : 1
|
||||
else
|
||||
tsl := math.min(dn1, tsl)
|
||||
if calcC > (tsl + thresholdAmount)
|
||||
bullMomentumBars := bullMomentumBars + 1
|
||||
bearMomentumBars := 0
|
||||
else
|
||||
bullMomentumBars := 0
|
||||
trend := bullMomentumBars >= (confirmBars + 1) ? 1 : -1
|
||||
|
||||
supertrend = tsl
|
||||
|
||||
// =============================================================================
|
||||
// INDICATORS
|
||||
// =============================================================================
|
||||
|
||||
// ADX
|
||||
upMove = calcH - calcH[1]
|
||||
downMove = calcL[1] - calcL
|
||||
plusDM = (upMove > downMove and upMove > 0) ? upMove : 0.0
|
||||
minusDM = (downMove > upMove and downMove > 0) ? downMove : 0.0
|
||||
trADX = math.max(calcH - calcL, math.max(math.abs(calcH - calcC[1]), math.abs(calcL - calcC[1])))
|
||||
atrADX = ta.rma(trADX, adxLen)
|
||||
plusDMSmooth = ta.rma(plusDM, adxLen)
|
||||
minusDMSmooth = ta.rma(minusDM, adxLen)
|
||||
plusDI = atrADX == 0.0 ? 0.0 : 100.0 * plusDMSmooth / atrADX
|
||||
minusDI = atrADX == 0.0 ? 0.0 : 100.0 * minusDMSmooth / atrADX
|
||||
dx = (plusDI + minusDI == 0.0) ? 0.0 : 100.0 * math.abs(plusDI - minusDI) / (plusDI + minusDI)
|
||||
adxVal = ta.rma(dx, adxLen)
|
||||
|
||||
// RSI
|
||||
rsi14 = ta.rsi(calcC, 14)
|
||||
|
||||
// Volume ratio
|
||||
volMA20 = ta.sma(volume, 20)
|
||||
volumeRatio = volume / volMA20
|
||||
|
||||
// Price position in 100-bar range
|
||||
highest100 = ta.highest(calcH, 100)
|
||||
lowest100 = ta.lowest(calcL, 100)
|
||||
priceRange = highest100 - lowest100
|
||||
pricePosition = priceRange == 0 ? 50.0 : ((calcC - lowest100) / priceRange) * 100
|
||||
|
||||
// =============================================================================
|
||||
// SHORT SIGNAL FILTERS
|
||||
// =============================================================================
|
||||
|
||||
adxOk = adxVal >= adxMin
|
||||
shortBufferOk = calcC < supertrend - entryBufferATR * atr
|
||||
rsiShortOk = rsi14 >= rsiShortMin and rsi14 <= rsiShortMax
|
||||
shortPositionOk = pricePosition > shortPosMin
|
||||
volumeOk = not useVolumeFilter or (volumeRatio >= volMin and volumeRatio <= volMax)
|
||||
|
||||
// =============================================================================
|
||||
// SHORT SIGNAL
|
||||
// =============================================================================
|
||||
|
||||
sellFlip = trend == -1 and trend[1] == 1
|
||||
sellReady = ta.barssince(sellFlip) == confirmBars
|
||||
|
||||
// FINAL SHORT SIGNAL (all filters applied)
|
||||
finalShortSignal = sellReady and adxOk and shortBufferOk and rsiShortOk and shortPositionOk and volumeOk
|
||||
|
||||
// =============================================================================
|
||||
// STRATEGY EXECUTION
|
||||
// =============================================================================
|
||||
|
||||
// Entry
|
||||
if finalShortSignal and strategy.position_size == 0
|
||||
strategy.entry("Short", strategy.short)
|
||||
|
||||
// Exit with TP/SL
|
||||
if strategy.position_size < 0
|
||||
entryPrice = strategy.position_avg_price
|
||||
tpPrice = entryPrice * (1 - tpPercent / 100)
|
||||
slPrice = entryPrice * (1 + slPercent / 100)
|
||||
strategy.exit("Exit", "Short", limit=tpPrice, stop=slPrice)
|
||||
|
||||
// =============================================================================
|
||||
// PLOTS
|
||||
// =============================================================================
|
||||
|
||||
upTrend = trend == 1 ? supertrend : na
|
||||
downTrend = trend == -1 ? supertrend : na
|
||||
|
||||
plot(upTrend, "Up Trend", color=color.new(color.green, 0), style=plot.style_linebr, linewidth=2)
|
||||
plot(downTrend, "Down Trend", color=color.new(color.red, 0), style=plot.style_linebr, linewidth=2)
|
||||
|
||||
plotshape(finalShortSignal, title="Short Signal", location=location.abovebar, color=color.red, style=shape.triangledown, size=size.small)
|
||||
|
||||
// =============================================================================
|
||||
// DEBUG TABLE
|
||||
// =============================================================================
|
||||
|
||||
var table dbg = table.new(position.top_right, 2, 8, bgcolor=color.new(color.black, 80))
|
||||
if barstate.islast
|
||||
table.cell(dbg, 0, 0, "MODE", text_color=color.white)
|
||||
table.cell(dbg, 1, 0, "SHORT ONLY", text_color=color.red)
|
||||
table.cell(dbg, 0, 1, "Trend", text_color=color.white)
|
||||
table.cell(dbg, 1, 1, trend == 1 ? "UP" : "DOWN ✓", text_color=trend == 1 ? color.gray : color.red)
|
||||
table.cell(dbg, 0, 2, "ADX", text_color=color.white)
|
||||
table.cell(dbg, 1, 2, str.tostring(adxVal, "#.#") + " >= " + str.tostring(adxMin) + (adxOk ? " ✓" : " ✗"), text_color=adxOk ? color.lime : color.red)
|
||||
table.cell(dbg, 0, 3, "RSI", text_color=color.white)
|
||||
table.cell(dbg, 1, 3, str.tostring(rsi14, "#.#") + " [" + str.tostring(rsiShortMin, "#") + "-" + str.tostring(rsiShortMax, "#") + "]" + (rsiShortOk ? " ✓" : " ✗"), text_color=rsiShortOk ? color.lime : color.red)
|
||||
table.cell(dbg, 0, 4, "Position", text_color=color.white)
|
||||
table.cell(dbg, 1, 4, str.tostring(pricePosition, "#.#") + "% > " + str.tostring(shortPosMin, "#") + "%" + (shortPositionOk ? " ✓" : " ✗"), text_color=shortPositionOk ? color.lime : color.red)
|
||||
table.cell(dbg, 0, 5, "Buffer", text_color=color.white)
|
||||
table.cell(dbg, 1, 5, shortBufferOk ? "OK ✓" : "—", text_color=shortBufferOk ? color.lime : color.gray)
|
||||
table.cell(dbg, 0, 6, "TP/SL", text_color=color.white)
|
||||
table.cell(dbg, 1, 6, str.tostring(tpPercent, "#.##") + "% / " + str.tostring(slPercent, "#.#") + "%", text_color=color.yellow)
|
||||
table.cell(dbg, 0, 7, "Signal", text_color=color.white)
|
||||
table.cell(dbg, 1, 7, finalShortSignal ? "SELL!" : "—", text_color=finalShortSignal ? color.red : color.gray)
|
||||
Reference in New Issue
Block a user