import random				#For shuffling the PnL
import gc				#For cleanup
import matplotlib.pyplot as plt		#For plotting the equity curves
import pandas as pd			#For data extraction and formatting
import numpy as np			#For calculations on extracted data
import argparse				#For input from CLI as well as help menu

# Argparse stuff
parser = argparse.ArgumentParser(
	prog="MonteCarloSimulator",
	formatter_class=argparse.RawDescriptionHelpFormatter,
	description="""
==================================================
|  MONTE CARLO SIMULATION AND ANALYSIS FOR MQL5  |
==================================================

Extracts PnL from MQL5 HTML Report files and runs a Monte Carlo simulation and analysis.
	""",
	epilog="""
Examples: python3 monteCarlo.py myReport.html 1000 myReport.png
	  python3 monteCarlo.py path_to_my_report/myReport.html 100000 path_to_save/myReport.png
	"""
)												#Setting up the help screen

parser.add_argument("report", help="Path to MQL5 HTML report")					#Report arguement
parser.add_argument("sampleSize", help="Sample Size/ Number of Runs for analysis", type=int)	#Sample Size arguement
parser.add_argument("output", help="Name and path for .png output")				#Name & location for output file

args = parser.parse_args()									#Stores data input from CLI

# Read the HTML report
table = pd.read_html(args.report, header=None, match="Deals")[1]

# Find where the Deals section starts
deals_start = table[table
		.astype(str)
		.apply(lambda r: r.str.contains("Deals", case=False, na=False)
		.any(), axis=1)
		].index[0]

# Use the Deals header row as column names
headers = table.iloc[deals_start + 1]
deals = table.iloc[deals_start + 2:].copy()
deals.columns = headers

# Extract Profit for closed ("out") deals
pnl = (
    deals
    .loc[deals["Direction"].astype(str).str.strip().str.lower() == "out", "Profit"]
    .astype(str)
    .str.replace(" ", "", regex=False)                                                      #Remove thousands separator
    .astype(float)
    .tolist()
)

#Obtain initial balance
initial_balance = 0
balance_row = deals.loc[deals["Type"].astype(str).str.strip().str.lower() == "balance"]

if not balance_row.empty:
    initial_balance = (
        balance_row["Balance"]
        .astype(str)
        .str.replace(" ", "", regex=False)
        .astype(float)
        .iloc[0]
    )
else:
    print("Initial Balance Not Available!")

pnl_df = pd.DataFrame({"PnL": pnl})                                                         #Save the PnL data in a dataframe   

######################(MONTE CARLO MODULE)########################
trades = pnl_df
numberOfTrades = len(trades)-1

allEquityLines = []									   #Stores equity curve data for further analysis
allDrawDowns = []

def MonteCarlo(Trades, initialBalance, No):
	global bust								           #Bust rate variable
	global profit									   #Profit rate variable
	
	peakEquity = initial_balance							   #Peak Equity for max drawdown calculation
	maxDraw = 0	     								   #Max drawdown variable
	equity = initialBalance
	
	rNumbers = random.sample(range(0, No), No)					   #Creates a random order of indexes based on number of trades
	eY = []										   #Used to store equity values as the simulations occur
	
	for numbers in rNumbers:							   #Runs the simulation of an equity curve
		if equity <= 0:
			equity = 0
			eY.append(equity)
			maxDraw = initialBalance
		else:
			equity += round(Trades.iat[numbers, 0],2)			   #Add the PnL value to the equity after rounding it off
			eY.append(equity)
			
			if equity > peakEquity:						   #Updates peak equity variable
				peakEquity = equity
				
			drawdown = peakEquity - equity					   #Calculates current drawdown
			
			if drawdown > maxDraw:						   #Updates max drawdown variable		
				maxDraw = drawdown
			
	
	allEquityLines.append(eY)							   #Stores the equity curve data of the given run
	plt.plot(eY, alpha = 0.25)							   #Plots the equity curve
	
	allDrawDowns.append(maxDraw)							   #Store the max drawdown of the run
	
	#Update profit and bust variables
	if equity > initialBalance:
		profit += 1
	elif equity <= 0:
		bust += 1


x = 0											   #Sample Size counter
bust = 0										   #Bust rate variable
profit = 0										   #Profit rate variable
sampleSize = args.sampleSize								   #Sample size variable

#Monte Carlo simulation
while x < sampleSize:									   
	MonteCarlo(trades, initial_balance, numberOfTrades)				   #Generates equity curve
	x += 1										   #increases sample size counter

#Monte Carlo Analysis
allEquityLines = np.array(allEquityLines)						   #Stores all equty curve data into an array for further calculation
meanLine = np.mean(allEquityLines, axis = 0)					           #Calculate the mean equity curve
lowerBand = np.percentile(allEquityLines, 5, axis = 0)					   #Calculates the lower band for outlier identification
upperBand = np.percentile(allEquityLines, 95, axis = 0)					   #Calculates the upper band for outlier identification

plt.plot(meanLine, color = "red", linewidth = 3, label = "Mean Equity")			   #Plots mean equity curve
plt.fill_between(									   #Fill the non-outlier range
	range(len(meanLine)), 
	lowerBand, 
	upperBand, 
	alpha = 0.5, 
	color = "gray", 
	label="5-95% Range"
)
plt.xlabel("Trade Number")
plt.ylabel("Equity Curve")
plt.title(args.report)

plt.axhline(y = (initial_balance * 0.5), 						  #50% drawdown line
	color = "red", 
	linestyle = "--", 
	label = "50% Drawdown"
)
plt.axhline(y = initial_balance, 							  #Starting balance line
	color="black", 
	linestyle = "--", 
	label="Starting Balance"
)
plt.legend()										  #Display legend
plt.savefig(args.output)							          #Saves the generated image
#plt.show()										  #Definitive edition

maxDrawdown = max(allDrawDowns)								  #Obtains the max drawdown observed in the simulation

#Statistics
print("Bust Rate (%): ", (bust/sampleSize) * 100)					  #Bust Rate	
print("Profit Rate (%): ", (profit/sampleSize) * 100)					  #Profit Rate
print("Max Drawdown ($): ", round(maxDrawdown,2))					  #Max Drawdown

gc.collect()										  #Garbage Collection
