231 lines
9.7 KiB
Python
231 lines
9.7 KiB
Python
"""
|
|
Trailing Stop Loss Analysis
|
|
Compares fixed stop loss vs trailing stop loss performance
|
|
"""
|
|
import numpy as np
|
|
import matplotlib
|
|
matplotlib.use('Agg') # Use non-interactive backend
|
|
import matplotlib.pyplot as plt
|
|
import pandas as pd
|
|
from scipy.stats import norm
|
|
|
|
class TrailingStopAnalyzer:
|
|
def __init__(self, initial_price=100, drift=0.0001, volatility=0.02,
|
|
trailing_distance=0.02, fixed_stop_distance=0.02):
|
|
self.initial_price = initial_price
|
|
self.drift = drift
|
|
self.volatility = volatility
|
|
self.trailing_distance = trailing_distance
|
|
self.fixed_stop_distance = fixed_stop_distance
|
|
|
|
def simulate_price_path(self, num_steps=1000, dt=1/252):
|
|
"""Simulate price using geometric Brownian motion"""
|
|
prices = [self.initial_price]
|
|
|
|
for _ in range(num_steps):
|
|
dW = np.random.normal(0, np.sqrt(dt))
|
|
dS = self.drift * prices[-1] * dt + self.volatility * prices[-1] * dW
|
|
prices.append(prices[-1] + dS)
|
|
|
|
return np.array(prices)
|
|
|
|
def apply_fixed_stop(self, prices, stop_distance):
|
|
"""Apply fixed stop loss"""
|
|
stop_price = self.initial_price - stop_distance * self.initial_price
|
|
exit_idx = None
|
|
|
|
for i, price in enumerate(prices):
|
|
if price <= stop_price:
|
|
exit_idx = i
|
|
break
|
|
|
|
if exit_idx is None:
|
|
exit_price = prices[-1]
|
|
exit_idx = len(prices) - 1
|
|
else:
|
|
exit_price = stop_price
|
|
|
|
return exit_idx, exit_price
|
|
|
|
def apply_trailing_stop(self, prices, trailing_distance):
|
|
"""Apply trailing stop loss"""
|
|
stop_price = self.initial_price - trailing_distance * self.initial_price
|
|
exit_idx = None
|
|
|
|
for i, price in enumerate(prices):
|
|
# Update trailing stop (only moves up for long positions)
|
|
new_stop = price - trailing_distance * price
|
|
if new_stop > stop_price:
|
|
stop_price = new_stop
|
|
|
|
# Check if stop is hit
|
|
if price <= stop_price:
|
|
exit_idx = i
|
|
break
|
|
|
|
if exit_idx is None:
|
|
exit_price = prices[-1]
|
|
exit_idx = len(prices) - 1
|
|
else:
|
|
exit_price = stop_price
|
|
|
|
return exit_idx, exit_price, stop_price
|
|
|
|
def compare_strategies(self, num_simulations=1000, num_steps=1000):
|
|
"""Compare fixed vs trailing stop"""
|
|
results = []
|
|
|
|
for sim in range(num_simulations):
|
|
prices = self.simulate_price_path(num_steps)
|
|
|
|
# Fixed stop
|
|
fixed_exit_idx, fixed_exit_price = self.apply_fixed_stop(
|
|
prices, self.fixed_stop_distance)
|
|
fixed_return = (fixed_exit_price - self.initial_price) / self.initial_price
|
|
|
|
# Trailing stop
|
|
trailing_exit_idx, trailing_exit_price, final_stop = self.apply_trailing_stop(
|
|
prices, self.trailing_distance)
|
|
trailing_return = (trailing_exit_price - self.initial_price) / self.initial_price
|
|
|
|
results.append({
|
|
'simulation': sim,
|
|
'final_price': prices[-1],
|
|
'fixed_return': fixed_return,
|
|
'trailing_return': trailing_return,
|
|
'fixed_exit_time': fixed_exit_idx,
|
|
'trailing_exit_time': trailing_exit_idx,
|
|
'improvement': trailing_return - fixed_return
|
|
})
|
|
|
|
return pd.DataFrame(results)
|
|
|
|
def plot_comparison(self, num_simulations=1000):
|
|
"""Plot comparison results"""
|
|
results_df = self.compare_strategies(num_simulations)
|
|
|
|
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
|
|
|
|
# Plot 1: Return Distribution Comparison
|
|
axes[0, 0].hist(results_df['fixed_return'], bins=50, alpha=0.5,
|
|
label='Fixed Stop', color='red', edgecolor='black')
|
|
axes[0, 0].hist(results_df['trailing_return'], bins=50, alpha=0.5,
|
|
label='Trailing Stop', color='green', edgecolor='black')
|
|
axes[0, 0].axvline(0, color='black', linestyle='--', linewidth=1)
|
|
axes[0, 0].set_xlabel('Return')
|
|
axes[0, 0].set_ylabel('Frequency')
|
|
axes[0, 0].set_title('Return Distribution Comparison')
|
|
axes[0, 0].legend()
|
|
axes[0, 0].grid(True, alpha=0.3)
|
|
|
|
# Plot 2: Improvement Distribution
|
|
axes[0, 1].hist(results_df['improvement'], bins=50, color='blue',
|
|
edgecolor='black', alpha=0.7)
|
|
axes[0, 1].axvline(0, color='red', linestyle='--', linewidth=2,
|
|
label='No Improvement')
|
|
axes[0, 1].axvline(results_df['improvement'].mean(), color='green',
|
|
linestyle='--', linewidth=2,
|
|
label=f'Mean: {results_df["improvement"].mean():.4f}')
|
|
axes[0, 1].set_xlabel('Improvement (Trailing - Fixed)')
|
|
axes[0, 1].set_ylabel('Frequency')
|
|
axes[0, 1].set_title('Trailing Stop Improvement Distribution')
|
|
axes[0, 1].legend()
|
|
axes[0, 1].grid(True, alpha=0.3)
|
|
|
|
# Plot 3: Sample Price Path with Stops
|
|
sample_prices = self.simulate_price_path(500)
|
|
_, fixed_exit = self.apply_fixed_stop(sample_prices, self.fixed_stop_distance)
|
|
trailing_stops = []
|
|
current_stop = self.initial_price - self.trailing_distance * self.initial_price
|
|
|
|
for price in sample_prices:
|
|
new_stop = price - self.trailing_distance * price
|
|
if new_stop > current_stop:
|
|
current_stop = new_stop
|
|
trailing_stops.append(current_stop)
|
|
|
|
axes[1, 0].plot(sample_prices, 'b-', label='Price', linewidth=2)
|
|
axes[1, 0].axhline(self.initial_price - self.fixed_stop_distance * self.initial_price,
|
|
color='red', linestyle='--', label='Fixed Stop', linewidth=2)
|
|
axes[1, 0].plot(trailing_stops, 'g--', label='Trailing Stop', linewidth=2)
|
|
axes[1, 0].set_xlabel('Time Step')
|
|
axes[1, 0].set_ylabel('Price')
|
|
axes[1, 0].set_title('Sample Price Path with Stop Losses')
|
|
axes[1, 0].legend()
|
|
axes[1, 0].grid(True, alpha=0.3)
|
|
|
|
# Plot 4: Performance Metrics Comparison
|
|
metrics = ['Mean Return', 'Std Dev', 'Sharpe Ratio', 'Win Rate', 'Max Return']
|
|
fixed_vals = [
|
|
results_df['fixed_return'].mean(),
|
|
results_df['fixed_return'].std(),
|
|
results_df['fixed_return'].mean() / results_df['fixed_return'].std() if results_df['fixed_return'].std() > 0 else 0,
|
|
(results_df['fixed_return'] > 0).mean(),
|
|
results_df['fixed_return'].max()
|
|
]
|
|
trailing_vals = [
|
|
results_df['trailing_return'].mean(),
|
|
results_df['trailing_return'].std(),
|
|
results_df['trailing_return'].mean() / results_df['trailing_return'].std() if results_df['trailing_return'].std() > 0 else 0,
|
|
(results_df['trailing_return'] > 0).mean(),
|
|
results_df['trailing_return'].max()
|
|
]
|
|
|
|
x = np.arange(len(metrics))
|
|
width = 0.35
|
|
axes[1, 1].bar(x - width/2, fixed_vals, width, label='Fixed Stop', color='red', alpha=0.7)
|
|
axes[1, 1].bar(x + width/2, trailing_vals, width, label='Trailing Stop', color='green', alpha=0.7)
|
|
axes[1, 1].set_xlabel('Metric')
|
|
axes[1, 1].set_ylabel('Value')
|
|
axes[1, 1].set_title('Performance Metrics Comparison')
|
|
axes[1, 1].set_xticks(x)
|
|
axes[1, 1].set_xticklabels(metrics, rotation=45, ha='right')
|
|
axes[1, 1].legend()
|
|
axes[1, 1].grid(True, alpha=0.3, axis='y')
|
|
|
|
plt.tight_layout()
|
|
return fig, results_df
|
|
|
|
if __name__ == "__main__":
|
|
# Create analyzer
|
|
analyzer = TrailingStopAnalyzer(
|
|
initial_price=100,
|
|
drift=0.0001,
|
|
volatility=0.02,
|
|
trailing_distance=0.02,
|
|
fixed_stop_distance=0.02
|
|
)
|
|
|
|
print("Running Trailing Stop Analysis...")
|
|
fig, results_df = analyzer.plot_comparison(num_simulations=1000)
|
|
|
|
print("\n=== Trailing Stop vs Fixed Stop Analysis ===")
|
|
print(f"\nFixed Stop Results:")
|
|
print(f" Mean Return: {results_df['fixed_return'].mean():.4f}")
|
|
print(f" Std Dev: {results_df['fixed_return'].std():.4f}")
|
|
print(f" Sharpe Ratio: {results_df['fixed_return'].mean() / results_df['fixed_return'].std():.4f}")
|
|
print(f" Win Rate: {(results_df['fixed_return'] > 0).mean():.2%}")
|
|
|
|
print(f"\nTrailing Stop Results:")
|
|
print(f" Mean Return: {results_df['trailing_return'].mean():.4f}")
|
|
print(f" Std Dev: {results_df['trailing_return'].std():.4f}")
|
|
print(f" Sharpe Ratio: {results_df['trailing_return'].mean() / results_df['trailing_return'].std():.4f}")
|
|
print(f" Win Rate: {(results_df['trailing_return'] > 0).mean():.2%}")
|
|
|
|
print(f"\nImprovement:")
|
|
improvement = results_df['trailing_return'].mean() - results_df['fixed_return'].mean()
|
|
print(f" Mean Improvement: {improvement:.4f} ({improvement/results_df['fixed_return'].mean()*100:.1f}%)")
|
|
print(f" Improvement Frequency: {(results_df['improvement'] > 0).mean():.2%}")
|
|
|
|
import os
|
|
# Get the script directory and construct path to figures
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
figures_dir = os.path.join(script_dir, '..', 'figures')
|
|
figures_path = os.path.abspath(figures_dir)
|
|
os.makedirs(figures_path, exist_ok=True)
|
|
|
|
output_path = os.path.join(figures_path, 'trailing_stop_analysis.png')
|
|
plt.savefig(output_path, dpi=300, bbox_inches='tight')
|
|
print(f"\nFigure saved to {output_path}")
|
|
plt.close()
|