fix: Add TP1 TP2 SL prices to Telegram trade notifications

This commit is contained in:
mindesbunister
2026-01-07 08:39:24 +01:00
parent e2763d21f2
commit efbe4d0c04
6 changed files with 1675 additions and 6 deletions

View File

@@ -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()
})

View 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()

View 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()

View File

@@ -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",

View 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)

View 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)