From efbe4d0c042ee5e120d5838fdee82d2f332612c7 Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Wed, 7 Jan 2026 08:39:24 +0100 Subject: [PATCH] fix: Add TP1 TP2 SL prices to Telegram trade notifications --- app/api/analytics/version-comparison/route.ts | 16 +- backtester/v11_shorts_only.py | 766 ++++++++++++++++++ ha-setup/dns-failover-monitor-improved.py | 500 ++++++++++++ workflows/trading/Money_Machine.json | 2 +- .../mean_reversion_shorts_strategy.pinescript | 194 +++++ ...moneyline_v11_2_shorts_strategy.pinescript | 203 +++++ 6 files changed, 1675 insertions(+), 6 deletions(-) create mode 100644 backtester/v11_shorts_only.py create mode 100644 ha-setup/dns-failover-monitor-improved.py create mode 100644 workflows/trading/mean_reversion_shorts_strategy.pinescript create mode 100644 workflows/trading/moneyline_v11_2_shorts_strategy.pinescript diff --git a/app/api/analytics/version-comparison/route.ts b/app/api/analytics/version-comparison/route.ts index bebe196..7fb2875 100644 --- a/app/api/analytics/version-comparison/route.ts +++ b/app/api/analytics/version-comparison/route.ts @@ -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 = { - '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 = { - '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() }) diff --git a/backtester/v11_shorts_only.py b/backtester/v11_shorts_only.py new file mode 100644 index 0000000..48e681a --- /dev/null +++ b/backtester/v11_shorts_only.py @@ -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() diff --git a/ha-setup/dns-failover-monitor-improved.py b/ha-setup/dns-failover-monitor-improved.py new file mode 100644 index 0000000..fc528ea --- /dev/null +++ b/ha-setup/dns-failover-monitor-improved.py @@ -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 'AUTOMATIC FAILOVER ACTIVATED\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"Trading Bot: {'āœ… STARTED' if bot_started else 'āŒ FAILED TO START!'}\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āš ļø URGENT: 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"šŸ”„ AUTOMATIC FAILBACK COMPLETE\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"Secondary Bot: {'āœ… STOPPED' if bot_stopped else 'āš ļø May still be running!'}\n" + telegram_msg += f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n" + telegram_msg += f"āš ļø Manual Action Required:\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āŒ WARNING: 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() diff --git a/workflows/trading/Money_Machine.json b/workflows/trading/Money_Machine.json index 9d57583..4b2469f 100644 --- a/workflows/trading/Money_Machine.json +++ b/workflows/trading/Money_Machine.json @@ -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", diff --git a/workflows/trading/mean_reversion_shorts_strategy.pinescript b/workflows/trading/mean_reversion_shorts_strategy.pinescript new file mode 100644 index 0000000..0ff7954 --- /dev/null +++ b/workflows/trading/mean_reversion_shorts_strategy.pinescript @@ -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) diff --git a/workflows/trading/moneyline_v11_2_shorts_strategy.pinescript b/workflows/trading/moneyline_v11_2_shorts_strategy.pinescript new file mode 100644 index 0000000..1903256 --- /dev/null +++ b/workflows/trading/moneyline_v11_2_shorts_strategy.pinescript @@ -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)