213 lines
8.6 KiB
Python
213 lines
8.6 KiB
Python
"""
|
|
Martingale Strategy Simulation
|
|
Analyzes the statistical properties and risk of martingale strategies
|
|
"""
|
|
import numpy as np
|
|
import matplotlib
|
|
matplotlib.use('Agg') # Use non-interactive backend
|
|
import matplotlib.pyplot as plt
|
|
from scipy import stats
|
|
import pandas as pd
|
|
|
|
class MartingaleSimulator:
|
|
def __init__(self, initial_balance=10000, base_lot=0.01, win_prob=0.5,
|
|
win_amount=10, loss_amount=10, max_losses=10):
|
|
self.initial_balance = initial_balance
|
|
self.base_lot = base_lot
|
|
self.win_prob = win_prob
|
|
self.win_amount = win_amount
|
|
self.loss_amount = loss_amount
|
|
self.max_losses = max_losses
|
|
|
|
def calculate_position_size(self, consecutive_losses):
|
|
"""Calculate position size after n consecutive losses"""
|
|
return self.base_lot * (2 ** consecutive_losses)
|
|
|
|
def calculate_required_capital(self, consecutive_losses):
|
|
"""Calculate total capital needed after n losses"""
|
|
return self.base_lot * (2 ** (consecutive_losses + 1) - 1)
|
|
|
|
def simulate_trade_sequence(self, num_trades=1000):
|
|
"""Simulate a sequence of trades"""
|
|
balance = self.initial_balance
|
|
consecutive_losses = 0
|
|
trades = []
|
|
ruin = False
|
|
|
|
for i in range(num_trades):
|
|
if balance <= 0:
|
|
ruin = True
|
|
break
|
|
|
|
# Calculate position size
|
|
position_size = self.calculate_position_size(consecutive_losses)
|
|
required_capital = self.calculate_required_capital(consecutive_losses)
|
|
|
|
# Check if we have enough capital
|
|
if required_capital > balance:
|
|
ruin = True
|
|
break
|
|
|
|
# Simulate trade outcome
|
|
is_win = np.random.random() < self.win_prob
|
|
|
|
if is_win:
|
|
# Win: recover all previous losses
|
|
profit = position_size * self.win_amount
|
|
balance += profit
|
|
consecutive_losses = 0
|
|
outcome = 'Win'
|
|
else:
|
|
# Loss: add to consecutive losses
|
|
loss = position_size * self.loss_amount
|
|
balance -= loss
|
|
consecutive_losses += 1
|
|
outcome = 'Loss'
|
|
|
|
trades.append({
|
|
'trade': i + 1,
|
|
'balance': balance,
|
|
'position_size': position_size,
|
|
'consecutive_losses': consecutive_losses,
|
|
'outcome': outcome,
|
|
'profit': profit if is_win else -loss
|
|
})
|
|
|
|
return pd.DataFrame(trades), ruin
|
|
|
|
def monte_carlo_analysis(self, num_simulations=1000, num_trades=100):
|
|
"""Run Monte Carlo simulation"""
|
|
results = []
|
|
ruin_count = 0
|
|
|
|
for sim in range(num_simulations):
|
|
trades_df, ruin = self.simulate_trade_sequence(num_trades)
|
|
if ruin:
|
|
ruin_count += 1
|
|
final_balance = 0
|
|
else:
|
|
final_balance = trades_df['balance'].iloc[-1]
|
|
|
|
results.append({
|
|
'simulation': sim,
|
|
'final_balance': final_balance,
|
|
'ruin': ruin,
|
|
'total_trades': len(trades_df),
|
|
'max_consecutive_losses': trades_df['consecutive_losses'].max() if len(trades_df) > 0 else 0
|
|
})
|
|
|
|
return pd.DataFrame(results), ruin_count / num_simulations
|
|
|
|
def plot_simulation_results(self, num_simulations=100):
|
|
"""Plot simulation results"""
|
|
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
|
|
|
|
# Run simulations
|
|
results_df, ruin_prob = self.monte_carlo_analysis(num_simulations)
|
|
|
|
# Plot 1: Final Balance Distribution
|
|
axes[0, 0].hist(results_df['final_balance'], bins=50, edgecolor='black')
|
|
axes[0, 0].axvline(self.initial_balance, color='red', linestyle='--',
|
|
label=f'Initial Balance: ${self.initial_balance:,.0f}')
|
|
axes[0, 0].set_xlabel('Final Balance ($)')
|
|
axes[0, 0].set_ylabel('Frequency')
|
|
axes[0, 0].set_title(f'Final Balance Distribution\nRuin Probability: {ruin_prob:.2%}')
|
|
axes[0, 0].legend()
|
|
axes[0, 0].grid(True, alpha=0.3)
|
|
|
|
# Plot 2: Ruin Probability vs Consecutive Losses
|
|
max_losses_range = range(1, self.max_losses + 1)
|
|
ruin_probs = []
|
|
for n in max_losses_range:
|
|
required = self.calculate_required_capital(n)
|
|
ruin_probs.append(1.0 if required > self.initial_balance else 0.0)
|
|
|
|
axes[0, 1].plot(max_losses_range, ruin_probs, 'ro-', linewidth=2, markersize=8)
|
|
axes[0, 1].set_xlabel('Consecutive Losses')
|
|
axes[0, 1].set_ylabel('Ruin Probability')
|
|
axes[0, 1].set_title('Ruin Probability vs Consecutive Losses')
|
|
axes[0, 1].grid(True, alpha=0.3)
|
|
axes[0, 1].set_ylim([-0.1, 1.1])
|
|
|
|
# Plot 3: Position Size Growth
|
|
losses_range = range(0, self.max_losses + 1)
|
|
position_sizes = [self.calculate_position_size(n) for n in losses_range]
|
|
required_capital = [self.calculate_required_capital(n) for n in losses_range]
|
|
|
|
ax3_twin = axes[1, 0].twinx()
|
|
line1 = axes[1, 0].plot(losses_range, position_sizes, 'b-o',
|
|
label='Position Size', linewidth=2)
|
|
line2 = ax3_twin.plot(losses_range, required_capital, 'r-s',
|
|
label='Required Capital', linewidth=2)
|
|
|
|
axes[1, 0].set_xlabel('Consecutive Losses')
|
|
axes[1, 0].set_ylabel('Position Size (Lots)', color='b')
|
|
ax3_twin.set_ylabel('Required Capital ($)', color='r')
|
|
axes[1, 0].set_title('Position Size and Capital Requirements')
|
|
axes[1, 0].grid(True, alpha=0.3)
|
|
|
|
# Combine legends
|
|
lines = line1 + line2
|
|
labels = [l.get_label() for l in lines]
|
|
axes[1, 0].legend(lines, labels, loc='upper left')
|
|
|
|
# Plot 4: Sample Trade Sequence
|
|
sample_trades, _ = self.simulate_trade_sequence(50)
|
|
axes[1, 1].plot(sample_trades['trade'], sample_trades['balance'],
|
|
'g-', linewidth=2, label='Balance')
|
|
axes[1, 1].axhline(self.initial_balance, color='red', linestyle='--',
|
|
label='Initial Balance')
|
|
axes[1, 1].set_xlabel('Trade Number')
|
|
axes[1, 1].set_ylabel('Balance ($)')
|
|
axes[1, 1].set_title('Sample Trade Sequence (50 trades)')
|
|
axes[1, 1].legend()
|
|
axes[1, 1].grid(True, alpha=0.3)
|
|
|
|
plt.tight_layout()
|
|
return fig
|
|
|
|
if __name__ == "__main__":
|
|
# Create simulator
|
|
simulator = MartingaleSimulator(
|
|
initial_balance=10000,
|
|
base_lot=0.01,
|
|
win_prob=0.5,
|
|
win_amount=10,
|
|
loss_amount=10,
|
|
max_losses=10
|
|
)
|
|
|
|
# Run analysis
|
|
print("Running Martingale Simulation...")
|
|
results_df, ruin_prob = simulator.monte_carlo_analysis(num_simulations=1000, num_trades=100)
|
|
|
|
print(f"\n=== Martingale Strategy Analysis ===")
|
|
print(f"Initial Balance: ${simulator.initial_balance:,.2f}")
|
|
print(f"Win Probability: {simulator.win_prob:.1%}")
|
|
print(f"\nMonte Carlo Results (1000 simulations):")
|
|
print(f"Ruin Probability: {ruin_prob:.2%}")
|
|
print(f"Mean Final Balance: ${results_df['final_balance'].mean():,.2f}")
|
|
print(f"Median Final Balance: ${results_df['final_balance'].median():,.2f}")
|
|
print(f"Std Dev Final Balance: ${results_df['final_balance'].std():,.2f}")
|
|
print(f"Max Final Balance: ${results_df['final_balance'].max():,.2f}")
|
|
print(f"Min Final Balance: ${results_df['final_balance'].min():,.2f}")
|
|
|
|
# Calculate statistics
|
|
profitable_sims = (results_df['final_balance'] > simulator.initial_balance).sum()
|
|
print(f"\nProfitable Simulations: {profitable_sims}/{len(results_df)} ({profitable_sims/len(results_df):.1%})")
|
|
print(f"Average Max Consecutive Losses: {results_df['max_consecutive_losses'].mean():.2f}")
|
|
|
|
# Generate plots
|
|
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)
|
|
|
|
fig = simulator.plot_simulation_results(num_simulations=100)
|
|
output_path = os.path.join(figures_path, 'martingale_analysis.png')
|
|
plt.savefig(output_path, dpi=300, bbox_inches='tight')
|
|
print(f"\nFigure saved to {output_path}")
|
|
plt.close()
|