Stock-Algorithm-Back-Tester/strategies/deprecated/sma_offset_trigger/run1.py
2024-08-14 14:10:18 -05:00

811 lines
56 KiB
Python

import plotly.express as plt
import plotly.subplots as subplt
import plotly.graph_objects as goplt
#import matplotlib.pyplot as mpl
import numpy as np
import pandas as pd
import math
import time
from multiprocessing import Pool
from functools import partial
import itertools
def read_ticker_data(stock_settings={}, trading_settings={}):
'''
Open the file to parse the data into a dictionary of arrays.
Data is sorted under arrays with keys: "Datetime" "Open" "High" "Low" "Close"
'''
data = {
"Datetime": [],
"Open": [],
"High": [],
"Low": [],
"Close": [],
"Points": 0
}
# Which open depends on where you execute the file
file = open(f"..//..//Raw Data//{stock_settings['Ticker']} {stock_settings['Period']} {stock_settings['Interval']} Data.csv")
#file = open(f"Raw Data//{stock_settings['Ticker']} {stock_settings['Period']} {stock_settings['Interval']} Data.csv")
file_list = file.readlines()
if trading_settings["Intervals in Period"] == 0: # This is to tell us that we want to use all data available to simulate trading
trading_settings["Intervals in Period"] = len(file_list)-1-trading_settings["Start Interval"]
i = 0
header = True
for line in file_list:
if i >= trading_settings["Start Interval"] + trading_settings["Intervals in Period"]: # So that we don't graph past how far we traded
break
if header:
header = False
else:
line_elements = line.strip().split(",") # Remove the newline at the end and split
data["Datetime"].append(line_elements[0])
data["Open"].append(float(line_elements[1]))
data["High"].append(float(line_elements[2]))
data["Low"].append(float(line_elements[3]))
data["Close"].append(float(line_elements[4]))
i += 1
file.close()
data["Points"] = len(data["Datetime"])
return data
def generate_sma(reference_data, length=4, trading_settings={}):
'''Return the simple moving average data of an array of reference_data. Length is how many places to look backwards for the average.'''
sma_data = [reference_data[i] for i in range(max(trading_settings["Start Interval"], length))] # This is the array we will return of the simple moving averaged data.
# We will initialize it with the first values copied over since we can't average the first few.
#sma_data = [reference_data[i] for i in range(max(trading_settings["Start Interval"]-length, length))]
#sma_data = list(itertools.repeat(reference_data[max(trading_settings["Start Interval"], length)-1], max(trading_settings["Start Interval"]-length, length)))
for i in range(max(trading_settings["Start Interval"]-length, length), trading_settings["Start Interval"]+trading_settings["Intervals in Period"]):
sma_data.append(sum(reference_data[i-length:i]) / length)
#for i in range(trading_settings["Start Interval"]+trading_settings["Intervals in Period"], len(data)):
#sma_data.append(0)
return sma_data
def generate_offset(data, offset_percent, keys_to_use=[], trading_settings={}):
'''Create an upper and lower offset if specified. If more than one array of data us selected,
it looks at which one is higher/lower for the appropriate offset.'''
upper_offset = list(itertools.repeat(0, max(1, trading_settings["Start Interval"])))
lower_offset = list(itertools.repeat(0, max(1, trading_settings["Start Interval"])))
if len(keys_to_use) == 1: # Just one line to apply an offset
for i in range(max(1, trading_settings["Start Interval"]), trading_settings["Start Interval"]+trading_settings["Intervals in Period"]):
upper_offset.append((1+offset_percent/100)*data[keys_to_use[0]][i])
lower_offset.append((1-offset_percent/100)*data[keys_to_use[0]][i])
if len(keys_to_use) == 2: # 2 lines, therefore we need to check which is above the other
for i in range(max(1, trading_settings["Start Interval"]), trading_settings["Start Interval"]+trading_settings["Intervals in Period"]):
if data[keys_to_use[0]][i] > data[keys_to_use[1]][i]:
upper_offset.append((1+offset_percent/100)*data[keys_to_use[0]][i])
lower_offset.append((1-offset_percent/100)*data[keys_to_use[1]][i])
else:
upper_offset.append((1+offset_percent/100)*data[keys_to_use[1]][i])
lower_offset.append((1-offset_percent/100)*data[keys_to_use[0]][i])
#points = len(data["Datetime"])
#for i in range(trading_settings["Start Interval"]+trading_settings["Intervals in Period"], points):
#upper_offset.append(0)
#lower_offset.append(0)
return upper_offset, lower_offset
def crossover_events(data, keys_to_use=[], trading_settings={}):
'''Returns an array of 1 or 0 when the first line crosses over the second, and then vice versa.
It is looking at the first listed index to see how it crosses the second.'''
cross_aboves = list(itertools.repeat(0, max(1, trading_settings["Start Interval"])))
cross_belows = list(itertools.repeat(0, max(1, trading_settings["Start Interval"])))
above = data[keys_to_use[0]][0] > data[keys_to_use[1]][0] # Whether or not the first line is above the other
for i in range(max(1, trading_settings["Start Interval"]), trading_settings["Start Interval"]+trading_settings["Intervals in Period"]):
if above and data[keys_to_use[0]][i] < data[keys_to_use[1]][i]: # Crosses beneath
cross_belows.append(1)
above = False
else:
cross_belows.append(0)
if (not above) and data[keys_to_use[0]][i] > data[keys_to_use[1]][i]: # Crosses above
cross_aboves.append(1)
above = True
else:
cross_aboves.append(0)
#points = len(data["Datetime"])
#for i in range(trading_settings["Start Interval"]+trading_settings["Intervals in Period"], points):
#cross_aboves.append(0)
#cross_belows.append(1)
return cross_aboves, cross_belows
def trend_reversal_event(data, method=1, trading_settings={}, current_trend=0):
'''Returns an array of 1 or 0 when the trend reverses.
Method 1: just looking at the high and low values crossing the offset.'''
bullish_reversal = list(itertools.repeat(0, trading_settings["Start Interval"]))
bearish_reversal = list(itertools.repeat(0, trading_settings["Start Interval"]))
neutral_reversal = list(itertools.repeat(0, trading_settings["Start Interval"]))
if method == 1: # Just looking at the high and low values crossing the offset.
current_trend = current_trend # 0 for neutral (only when we just begin), 1 for bullish, -1 for bearish
for i in range(trading_settings["Start Interval"], trading_settings["Start Interval"]+trading_settings["Intervals in Period"]):
if data["High Crosses Upper Offset"][i] and data["Low Crosses Lower Offset"][i]: # In the even we have both triggers let's ignore it
bullish_reversal.append(0)
bearish_reversal.append(0)
neutral_reversal.append(0)
elif data["High Crosses Upper Offset"][i] and current_trend != 1: # Checking for bullish reversal
bullish_reversal.append(1)
bearish_reversal.append(0)
neutral_reversal.append(0)
current_trend = 1
elif data["Low Crosses Lower Offset"][i] and current_trend != -1: # Checking for bearish reversal
bullish_reversal.append(0)
bearish_reversal.append(1)
neutral_reversal.append(0)
current_trend = -1
else:
bullish_reversal.append(0)
bearish_reversal.append(0)
neutral_reversal.append(0)
#points = len(data["Datetime"])
#for i in range(trading_settings["Start Interval"]+trading_settings["Intervals in Period"], points):
#bullish_reversal.append(0)
#bearish_reversal.append(0)
#neutral_reversal.append(0)
return bullish_reversal, bearish_reversal, neutral_reversal
def walking_forward_value(data, just_final=0, trading_settings={}, trading_interval_starting_values={}):
'''Generates a single array of the value of the account. Reinvests profits by default.
Interval delay is how many intervals it took to get in position. Defaults to 1 interval delay.
Worst case by default assumes selling at interval's low and buying at intervals high. Set to 0 to trade at close of interval.
If just_final is set to 1 then there will be no arrays created, just the final value will be given.
start_interval is the index of the interval that we started trading, num_trade_intervals is how long we continued to trade.'''
if not just_final: # If we want all of the data including the arrays
current_position = trading_interval_starting_values["Current Position"] # How many shares we own. Positive for long, negative for short
cash_value = [trading_interval_starting_values["Cash Value"] for i in range(0, max(1, trading_settings["Interval Delay"], trading_settings["Start Interval"]))] # Running total to keep track of cash to buy and cash after shorting
position_value = [trading_interval_starting_values["Position Value"] for i in range(0, max(1, trading_settings["Interval Delay"], trading_settings["Start Interval"]))] # Running total of the account's position value
account_value = [cash_value[i] + position_value[i] for i in range(0, max(1, trading_settings["Interval Delay"], trading_settings["Start Interval"]))] # Running total of the account's value
if trading_settings["Reinvest"]:
for i in range(max([1, trading_settings["Interval Delay"], trading_settings["Start Interval"]]), min(len(data["Close"])-trading_settings["Interval Delay"], trading_settings["Start Interval"]+trading_settings["Intervals in Period"])):
cash_value.append(cash_value[i-1])
if data["Buy Signal"][i-trading_settings["Interval Delay"]] == 1: # We have a bullish trend reversal so we should buy long
if current_position == 0: # We are in no current position, no need to buy to close first
if trading_settings["Worst Case"]:
current_position = math.floor(cash_value[i] / data["High"][i]) # Number of shares to buy with our available money
cash_value[i] = cash_value[i] - current_position * data["High"][i] # Take away our cash for entering the long position
else: # Not worst case buying at high so we buy at open
current_position = math.floor(cash_value[i] / data["Open"][i]) # Number of shares to buy with our available money
cash_value[i] = cash_value[i] - current_position * data["Open"][i] # Take away our cash for entering the long position
if current_position < 0: # We are in a short position so we need to buy to close first
if trading_settings["Worst Case"]:
cash_value[i] = cash_value[i] + current_position * data["High"][i] # Take away our cash for closing the short position
current_position = math.floor(cash_value[i] / data["High"][i]) # Number of shares to buy with our available money
cash_value[i] = cash_value[i] - current_position * data["High"][i] # Take away our cash for entering the long position
else: # Not worst case buying at high so we buy at open
cash_value[i] = cash_value[i] + current_position * data["Open"][i] # Take away our cash for closing the short position
current_position = math.floor(cash_value[i] / data["Open"][i]) # Number of shares to buy with our available money
cash_value[i] = cash_value[i] - current_position * data["Open"][i] # Take away our cash for entering the long position
if data["Sell Signal"][i-trading_settings["Interval Delay"]] == 1: # We have a bearish trend reversal so we should sell short
if current_position == 0: # We are in no current position, no need to sell to close first
if trading_settings["Worst Case"]:
current_position = -1*math.floor(cash_value[i] / data["Low"][i])/2 # Number of shares to sell short with our available money
cash_value[i] = cash_value[i] - current_position * data["Low"][i] # Add cash for selling short
else: # Not worst case buying at high so we buy at open
current_position = -1*math.floor(cash_value[i] / data["Open"][i])/2 # Number of shares to buy with our available money
cash_value[i] = cash_value[i] - current_position * data["Open"][i] # Add cash for selling short
if current_position > 0: # We are in a long position so we need to sell to close first
if trading_settings["Worst Case"]:
cash_value[i] = cash_value[i] + current_position * data["Low"][i] # Add cash for closing the long position
current_position = -1*math.floor(cash_value[i] / data["Low"][i])/2 # Number of shares to buy with our available money
cash_value[i] = cash_value[i] - current_position * data["Low"][i] # Add cash for selling short
else: # Not worst case buying at high so we buy at open
cash_value[i] = cash_value[i] + current_position * data["Open"][i] # Add cash for closing the long position
current_position = -1*math.floor(cash_value[i] / data["Open"][i])/2 # Number of shares to buy with our available money
cash_value[i] = cash_value[i] - current_position * data["Open"][i] # Add cash for selling short
position_value.append(current_position * data["Close"][i])
account_value.append(cash_value[i] + position_value[i])
# Needed to account for the last bit of data not added
#for i in range(max(trading_settings["Interval Delay"], len(data["Close"])-trading_settings["Intervals in Period"]-trading_settings["Start Interval"])):
#account_value.append(account_value[-1])
#cash_value.append(cash_value[-1])
#position_value.append(position_value[-1])
return account_value, cash_value, position_value, current_position
else: # All we want is the final value
current_position = 0 # How many shares we own. Positive for long, negative for short
cash_value = trading_settings["Starting Value"] # Running total to keep track of cash to buy and cash after shorting
position_value = 0 # Running total of the account's position value
account_value = trading_settings["Starting Value"] # Running total of the account's value
if trading_settings["Reinvest"]:
for i in range(max([1, trading_settings["Interval Delay"], trading_settings["Start Interval"]]), min(len(data["Close"])-trading_settings["Interval Delay"], trading_settings["Start Interval"]+trading_settings["Intervals in Period"])):
if data["Buy Signal"][i-trading_settings["Interval Delay"]] == 1: # We have a bullish trend reversal so we should buy long
if current_position == 0: # We are in no current position, no need to buy to close first
if trading_settings["Worst Case"]:
current_position = math.floor(cash_value / data["High"][i]) # Number of shares to buy with our available money
cash_value = cash_value - current_position * data["High"][i] # Take away our cash for entering the long position
else: # Not worst case buying at high so we buy at close
current_position = math.floor(cash_value / data["Open"][i]) # Number of shares to buy with our available money
cash_value = cash_value - current_position * data["Open"][i] # Take away our cash for entering the long position
if current_position < 0: # We are in a short position so we need to buy to close first
if trading_settings["Worst Case"]:
cash_value = cash_value + current_position * data["High"][i] # Take away our cash for closing the short position
current_position = math.floor(cash_value / data["High"][i]) # Number of shares to buy with our available money
cash_value = cash_value - current_position * data["High"][i] # Take away our cash for entering the long position
else: # Not worst case buying at high so we buy at close
cash_value = cash_value + current_position * data["Open"][i] # Take away our cash for closing the short position
current_position = math.floor(cash_value / data["Open"][i]) # Number of shares to buy with our available money
cash_value = cash_value - current_position * data["Open"][i] # Take away our cash for entering the long position
if data["Sell Signal"][i-trading_settings["Interval Delay"]] == 1: # We have a bearish trend reversal so we should sell short
if current_position == 0: # We are in no current position, no need to sell to close first
if trading_settings["Worst Case"]:
current_position = -1*math.floor(cash_value / data["Low"][i])/2 # Number of shares to sell short with our available money
cash_value = cash_value - current_position * data["Low"][i] # Add cash for selling short
else: # Not worst case buying at high so we buy at close
current_position = -1*math.floor(cash_value / data["Open"][i])/2 # Number of shares to buy with our available money
cash_value = cash_value - current_position * data["Close"][i] # Add cash for selling short
if current_position > 0: # We are in a long position so we need to sell to close first
if trading_settings["Worst Case"]:
cash_value = cash_value + current_position * data["Low"][i] # Add cash for closing the long position
current_position = -1*math.floor(cash_value / data["Low"][i])/2 # Number of shares to buy with our available money
cash_value = cash_value - current_position * data["Low"][i] # Add cash for selling short
else: # Not worst case buying at high so we buy at close
cash_value = cash_value + current_position * data["Open"][i] # Add cash for closing the long position
current_position = -1*math.floor(cash_value / data["Open"][i])/2 # Number of shares to buy with our available money
cash_value = cash_value - current_position * data["Open"][i] # Add cash for selling short
position_value = current_position * data["Close"][i]
account_value = cash_value + position_value
return round(account_value, 3)
def plot_values(data, keys_to_plot=[], colors=[], title=""):
'''Plots the specified values in data following keys_to_plot.
Single graph where all values are overlayed on each other'''
points = data["Points"]
plot_data = {
'Time Interval': np.linspace(0, stop=points, num=points, endpoint=False),
}
for key in keys_to_plot:
plot_data[key] = data[key]
df = pd.DataFrame(plot_data)
fig = plt.line(
df,
x = 'Time Interval',
y = keys_to_plot,
color_discrete_sequence = colors,
title = title
)
fig.show()
def plot_values_stacked(data, keys_to_plot=[], colors=[], title="", x_values=[]):
'''Plots the specified values in data following keys_to_plot where each sub array is included on their own graph.
Multiple, stacked graphs where all share the x-axis.'''
# Generate the subplot titles
subplot_titles = []
for subplot in keys_to_plot:
subplot_titles.append(", ".join(subplot))
# Setup the whole plots
fig = subplt.make_subplots(rows=len(keys_to_plot), cols=1, shared_xaxes=True, vertical_spacing=0.03, x_title="Interval", y_title="Value", subplot_titles=subplot_titles)
# Check to see if we passed along specific x values
if x_values:
subplot_num = 0
for subplot in keys_to_plot:
element_num = 0
for element in subplot:
fig.add_trace(goplt.Scatter(x=x_values[subplot_num], y=data[element], name=element, marker_color=colors[subplot_num][element_num]), row=subplot_num+1, col=1)
element_num += 1
subplot_num += 1
else:
# Add the traces for everything desired to be plotted
points = data["Points"]
x_values = np.linspace(0, stop=points, num=points, endpoint=False)
subplot_num = 0
for subplot in keys_to_plot:
element_num = 0
for element in subplot:
fig.add_trace(goplt.Scatter(x=x_values, y=data[element], name=element, marker_color=colors[subplot_num][element_num]), row=subplot_num+1, col=1)
element_num += 1
subplot_num += 1
fig.update_layout(title_text=title)
fig.show()
def optimize_algo_settings(data, optimization_settings={}, current_trend=0):
'''Optimizes the parameters for the algorithm by going through every possible value and comparing it to the highest seen value after going through a specific time interval.'''
best_account_value = optimization_settings["Starting Value"] # Keep track of highest ending value
best_algo_settings = [1,1,100] # Keep track of best settings, set default so if there is no setting that satisfies the condition, then it gives up trading
best_algo_settings_absolute = [1,1,100] # This is the best possible parameters, hands down. No trade frequency checking or anything else
intervals_per_trade = 0
# Reset the sma lines, offset lines, crossover events, and trend reversals
new_test_values = { # Update the values
"Start Interval": optimization_settings["Start Interval"],
"Intervals in Period": optimization_settings["Intervals in Period"],
"Short SMA": optimization_settings["Short SMA Range"][0],
"Long SMA": optimization_settings["Long SMA Range"][0],
"Offset Percent": optimization_settings["Offset Percent Range"][0],
"Interval Delay": optimization_settings["Interval Delay"],
"Reinvest": optimization_settings["Reinvest"],
"Worst Case": optimization_settings["Worst Case"],
"Starting Value": optimization_settings["Starting Value"]
}
for i in range(optimization_settings["Short SMA Range"][0], optimization_settings["Short SMA Range"][1]+1, optimization_settings["Short SMA Step"]):
new_test_values["Short SMA"] = i
data["Short SMA"] = generate_sma(data["Close"], i, new_test_values)
for j in range(optimization_settings["Long SMA Range"][0], optimization_settings["Long SMA Range"][1]+1, optimization_settings["Long SMA Step"]):
if j <= i: # Long sma is shorter than short sma so skip
continue
if i >= j: # Short sma is longer than the long sma so skip
continue
new_test_values["Long SMA"] = j
data["Long SMA"] = generate_sma(data["Close"], j, new_test_values)
offset_percent = optimization_settings["Offset Percent Range"][0] # Store the offset_percent since it is a float and can't be used for the iterator
for k in range(0, int((optimization_settings["Offset Percent Range"][1]-optimization_settings["Offset Percent Range"][0])/optimization_settings["Offset Percent Step"])):
new_test_values["Offset Percent"] = offset_percent
data["Upper SMA Offset"], data["Lower SMA Offset"] = generate_offset(data, offset_percent, ["Short SMA", "Long SMA"], new_test_values)
data["High Crosses Upper Offset"] = crossover_events(data, ["Open", "Upper SMA Offset"], new_test_values)[0]
data["Low Crosses Lower Offset"] = crossover_events(data, ["Open", "Lower SMA Offset"], new_test_values)[1]
data["Buy Signal"] = trend_reversal_event(data, 1, new_test_values,current_trend)[0]
data["Sell Signal"] = trend_reversal_event(data, 1, new_test_values, current_trend)[1]
try:
intervals_per_trade = (optimization_settings["Intervals in Period"])/(data["Buy Signal"].count(1) + data["Sell Signal"].count(1))
except:
intervals_per_trade = 0
account_value = walking_forward_value(data, 1, new_test_values)
if account_value > best_account_value: # We found a better parameter setup
if intervals_per_trade >= optimization_settings["Minimum Intervals Per Trade"]: # With not an unreasonable amount of trades
best_account_value = account_value
best_algo_settings = [i, j, round(offset_percent, 2)]
best_algo_settings_absolute = [i, j, round(offset_percent, 2)]
else:
best_algo_settings_absolute = [i, j, round(offset_percent, 2)]
offset_percent += round(optimization_settings["Offset Percent Step"], 3)
return best_algo_settings, round(best_account_value/optimization_settings["Starting Value"]*100 - 100, 3), best_algo_settings_absolute # Return best settings and the percent increase and the hands down best parameters
def multithreadable_algo_test(algo_params=[], optimization_settings={}, data={}):
'''Optimizes the parameters for the algorithm by going through every possible value and comparing it to the highest seen value after going through a specific time interval.'''
# Reset the sma lines, offset lines, crossover events, and trend reversals
new_test_values = { # Update the values
"Start Interval": optimization_settings["Start Interval"],
"Intervals in Period": optimization_settings["Intervals in Period"],
"Short SMA": algo_params[0],
"Long SMA": algo_params[1],
"Offset Percent": algo_params[2],
"Interval Delay": optimization_settings["Interval Delay"],
"Reinvest": optimization_settings["Reinvest"],
"Worst Case": optimization_settings["Worst Case"],
"Starting Value": optimization_settings["Starting Value"]
}
data["Short SMA"] = generate_sma(data["Close"], algo_params[0], new_test_values)
data["Long SMA"] = generate_sma(data["Close"], algo_params[1], new_test_values)
data["Upper SMA Offset"], data["Lower SMA Offset"] = generate_offset(data, algo_params[2], ["Short SMA", "Long SMA"], new_test_values)
data["High Crosses Upper Offset"] = crossover_events(data, ["Open", "Upper SMA Offset"], new_test_values)[0]
data["Low Crosses Lower Offset"] = crossover_events(data, ["Open", "Lower SMA Offset"], new_test_values)[1]
data["Buy Signal"] = trend_reversal_event(data, 1, new_test_values)[0]
data["Sell Signal"] = trend_reversal_event(data, 1, new_test_values)[1]
try:
intervals_per_trade = (optimization_settings["Intervals in Period"])/(data["Buy Signal"].count(1) + data["Sell Signal"].count(1))
except:
intervals_per_trade = 0
account_value = walking_forward_value(data, 1, new_test_values)
if intervals_per_trade >= optimization_settings["Minimum Intervals Per Trade"]: # With not an unreasonable amount of trades
return account_value
else:
return algo_params[-1]
def optimize_algo_settings_multithreaded(data={}, optimization_settings={}):
account_values_per_combo = []
with Pool(optimization_settings["Pools"]) as p:
account_values_per_combo = p.map(partial(multithreadable_algo_test, optimization_settings=optimization_settings, data=data), optimization_settings["Optimization Combinations"])
best_algo_settings = optimization_settings["Optimization Combinations"][account_values_per_combo.index(max(account_values_per_combo))]
return best_algo_settings
def multithreadable_algo_test_plottable(algo_params=[], optimization_settings={}, data={}):
'''Tests each algo param passed to it through multiprocessing. Returns a dictionary for every combination of parameters to be able to plot.'''
# Reset the sma lines, offset lines, crossover events, and trend reversals
new_test_values = { # Update the values
"Start Interval": optimization_settings["Start Interval"],
"Intervals in Period": optimization_settings["Intervals in Period"],
"Short SMA": algo_params[0],
"Long SMA": algo_params[1],
"Offset Percent": algo_params[2],
"Interval Delay": optimization_settings["Interval Delay"],
"Reinvest": optimization_settings["Reinvest"],
"Worst Case": optimization_settings["Worst Case"],
"Starting Value": optimization_settings["Starting Value"]
}
data["Short SMA"] = generate_sma(data["Close"], algo_params[0], new_test_values)
data["Long SMA"] = generate_sma(data["Close"], algo_params[1], new_test_values)
data["Upper SMA Offset"], data["Lower SMA Offset"] = generate_offset(data, algo_params[2], ["Short SMA", "Long SMA"], new_test_values)
data["High Crosses Upper Offset"] = crossover_events(data, ["Open", "Upper SMA Offset"], new_test_values)[0]
data["Low Crosses Lower Offset"] = crossover_events(data, ["Open", "Lower SMA Offset"], new_test_values)[1]
data["Buy Signal"] = trend_reversal_event(data, 1, new_test_values)[0]
data["Sell Signal"] = trend_reversal_event(data, 1, new_test_values)[1]
try:
intervals_per_trade = (optimization_settings["Intervals in Period"])/(data["Buy Signal"].count(1) + data["Sell Signal"].count(1))
except:
intervals_per_trade = 0
account_value = walking_forward_value(data, 1, new_test_values)
return_dict = {
"Algo Settings": algo_params,
"Final Account Value": account_value,
"Percent Increase": ((account_value/optimization_settings["Starting Value"])-1)*100,
"Number of Trades": data["Buy Signal"].count(1) + data["Sell Signal"].count(1),
"Avg Intervals Per Trade": intervals_per_trade
}
return return_dict
def optimize_algo_settings_with_plot(data={}, optimization_settings={}):
results = []
with Pool(optimization_settings["Pools"]) as p:
results = p.map(partial(multithreadable_algo_test_plottable, optimization_settings=optimization_settings, data=data), optimization_settings["Optimization Combinations"])
# Still return the best algo setting. Also need the best algo settings for the plot
best_algo_settings = []
best_percent_increase = 0
for result in results:
if result["Percent Increase"] > best_percent_increase:
best_algo_settings = result["Algo Settings"]
best_percent_increase = result["Percent Increase"]
# Plotting
# Format first
short_sma_title = f"Changing Short SMA with LSMA({best_algo_settings[1]}) OP({best_algo_settings[2]})"
long_sma_title = f"Changing Long SMA with SSMA({best_algo_settings[0]}) OP({best_algo_settings[2]})"
offset_percent_title = f"Changing Offset Percent with SSMA({best_algo_settings[0]}) LSMA({best_algo_settings[1]})"
plottable_results = {
short_sma_title: [],
long_sma_title: [],
offset_percent_title: [],
"Points": 0
}
x_values = {
"Main X Values": [],
"Short SMA X Values": [],
"Long SMA X Values": [],
"Offset Percent X Values": [],
}
for result in results:
# Short SMA first
if result["Algo Settings"][1] == best_algo_settings[1] and result["Algo Settings"][2] == best_algo_settings[2]: # Only graph values where the other 2 params were the "best"
plottable_results[short_sma_title].append(result["Percent Increase"])
x_values["Short SMA X Values"].append(result["Algo Settings"][0])
# Long SMA
if result["Algo Settings"][0] == best_algo_settings[0] and result["Algo Settings"][2] == best_algo_settings[2]: # Only graph values where the other 2 params were the "best"
plottable_results[long_sma_title].append(result["Percent Increase"])
x_values["Long SMA X Values"].append(result["Algo Settings"][1])
# Percent offset
if result["Algo Settings"][0] == best_algo_settings[0] and result["Algo Settings"][1] == best_algo_settings[1]: # Only graph values where the other 2 params were the "best"
plottable_results[offset_percent_title].append(result["Percent Increase"])
x_values["Offset Percent X Values"].append(result["Algo Settings"][2])
plottable_results["Points"] = max(len(plottable_results[short_sma_title]), len(plottable_results[long_sma_title]), len(plottable_results[offset_percent_title]))
x_values["Main X Values"] = np.linspace(0, stop=plottable_results["Points"], num=plottable_results["Points"], endpoint=False)
title = "Optimization Graphs"
plot_values_stacked(plottable_results, [[short_sma_title, long_sma_title, offset_percent_title], [short_sma_title], [long_sma_title], [offset_percent_title]], [["blue", "orange", "black"], ["blue"], ["orange"], ["black"]], title, [x_values["Main X Values"], x_values["Short SMA X Values"], x_values["Long SMA X Values"], x_values["Offset Percent X Values"]])
return best_algo_settings
def rolling_optimization_then_trade(stock_settings={}, trading_settings={}, optimization_settings={}, rollover_trading_settings={}):
'''Uses past data to optimize the parameters, then trades using those parameters for a specified period before reoptimizing and trading for another period...'''
data = read_ticker_data(stock_settings, optimization_settings)
# Check to see if our numbers are okay and use all available data if that was chosen
if len(data["Close"]) < rollover_trading_settings["Start Interval"] + rollover_trading_settings["Intervals to Test"]:
print("Start Rollover Interval + Number of Intervals is more than the amount of data we have")
return
if rollover_trading_settings["Intervals to Test"] == 0: # We want to use all available data
rollover_trading_settings["Intervals to Test"] = len(data["Close"]) - rollover_trading_settings["Intervals to Test"]
# Here is the for loop for every individual trading period we will have depending on how long we want to train before each period and how many data points we have
total_trading_periods = math.floor((rollover_trading_settings["Intervals to Test"]-rollover_trading_settings["Training Period Width"])/rollover_trading_settings["Trading Period Width"])
training_period_start_index = rollover_trading_settings["Start Interval"] # Keep track of the start of each training period
trading_period_start_index = training_period_start_index + rollover_trading_settings["Training Period Width"] # Keep track of the start of each trading period
optimization_settings["Intervals in Period"] = rollover_trading_settings["Training Period Width"] # Just a quick update to use the same training period width
optimization_settings["Start Interval"] = training_period_start_index # Update where we want to begin the training
trading_settings["Intervals in Period"] = rollover_trading_settings["Trading Period Width"]
trading_settings["Starting Value"] = rollover_trading_settings["Starting Value"]
trading_settings["Start Interval"] = trading_period_start_index
plottable_data = { # If we want to plot then we need to keep arrays of the data
"Datetime": [i for i in range(len(data["Close"]))],
"Open": [], # Basically a duplicate of the values we used, redundant but makes it easier to keep everything in this dictionary
"Close": [],
"High": [],
"Low": [],
"Short SMA Length": ["" for i in range(trading_settings["Start Interval"])], # Where we actually get to non redundant data because we are always changing our parameters
"Long SMA Length": ["" for i in range(trading_settings["Start Interval"])],
"Offset Percent": ["" for i in range(trading_settings["Start Interval"])],
"Short SMA": ["" for i in range(trading_settings["Start Interval"])],
"Long SMA": ["" for i in range(trading_settings["Start Interval"])],
"Upper Offset": ["" for i in range(trading_settings["Start Interval"])],
"Lower Offset": ["" for i in range(trading_settings["Start Interval"])],
"Buy Signal": ["" for i in range(trading_settings["Start Interval"])],
"Sell Signal": ["" for i in range(trading_settings["Start Interval"])],
"Account Value": ["" for i in range(trading_settings["Start Interval"])],
"Cash Value": ["" for i in range(trading_settings["Start Interval"])],
"Position Value": ["" for i in range(trading_settings["Start Interval"])],
"Trade Period Marker": ["0" for i in range(len(data["Close"]))],
"Points": data["Points"]
}
account_value = rollover_trading_settings["Starting Value"] # Keep track of our money so we can reinvest
trading_interval_starting_values = { # This will be fed into walking forward values so we don't lose our cash and position values
"Cash Value": rollover_trading_settings["Starting Value"],
"Position Value": 0,
"Account Value": rollover_trading_settings["Starting Value"],
"Current Trend": 0, # Bullish vs bearish vs neutral
"Current Position": 0
}
if optimization_settings["Multithreaded Training"]: # Use multithreading
# Generate all of the combinations
for i in range(optimization_settings["Short SMA Range"][0], optimization_settings["Short SMA Range"][1]+1):
for j in range(optimization_settings["Long SMA Range"][0], optimization_settings["Long SMA Range"][1]+1):
if j <= i: # Long sma is shorter than short sma so skip
continue
if i >= j: # Short sma is longer than the long sma so skip
continue
offset_percent = optimization_settings["Offset Percent Range"][0] # Store the offset_percent since it is a float and can't be used for the iterator
for k in range(0, int((optimization_settings["Offset Percent Range"][1]-optimization_settings["Offset Percent Range"][0])/optimization_settings["Offset Percent Step"])):
optimization_settings["Optimization Combinations"].append([i, j, round(offset_percent, 3)])
offset_percent += round(optimization_settings["Offset Percent Step"], 3)
for i in range(total_trading_periods):
algo_time = time.time()
print("Optimizing Algorithm took ", end="")
if optimization_settings["Multithreaded Training"]: # Use multithreading
if optimization_settings["Graph Optimizations"]:
best_algo_settings = optimize_algo_settings_with_plot(data, optimization_settings)
else:
best_algo_settings = optimize_algo_settings_multithreaded(data, optimization_settings)
else: # Don't use multithreading
best_algo_settings = optimize_algo_settings(data, optimization_settings, trading_interval_starting_values["Current Trend"])[0] # Get the best algorithm settings
print("%s seconds ---" % (time.time() - algo_time))
# Regenerate our data from the new parameters so we can trade it
data["Short SMA"] = generate_sma(data["Close"], best_algo_settings[0], trading_settings)
data["Long SMA"] = generate_sma(data["Close"], best_algo_settings[1], trading_settings)
data["Upper SMA Offset"], data["Lower SMA Offset"] = generate_offset(data, best_algo_settings[2], ["Short SMA", "Long SMA"], trading_settings)
data["High Crosses Upper Offset"] = crossover_events(data, ["Open", "Upper SMA Offset"], trading_settings)[0]
data["Low Crosses Lower Offset"] = crossover_events(data, ["Open", "Lower SMA Offset"], trading_settings)[1]
data["Buy Signal"] = trend_reversal_event(data, 1, trading_settings, trading_interval_starting_values["Current Trend"])[0]
data["Sell Signal"] = trend_reversal_event(data, 1, trading_settings, trading_interval_starting_values["Current Trend"])[1]
if rollover_trading_settings["Just Final"]: # All we want is the final values, no plotting
account_value = walking_forward_value(data, 1, trading_settings, trading_interval_starting_values)
account_value = round(account_value, 3)
else: # We want to be able to plot
account_value_array, cash_value_array, position_value_array, current_position = walking_forward_value(data, 0, trading_settings, trading_interval_starting_values)
account_value = round(account_value_array[-1], 3)
# Edit the length
data["Short SMA"] = data["Short SMA"][trading_settings["Start Interval"]:trading_settings["Start Interval"]+rollover_trading_settings["Trading Period Width"]]
data["Long SMA"] = data["Long SMA"][trading_settings["Start Interval"]:trading_settings["Start Interval"]+rollover_trading_settings["Trading Period Width"]]
data["Upper SMA Offset"] = data["Upper SMA Offset"][trading_settings["Start Interval"]:trading_settings["Start Interval"]+rollover_trading_settings["Trading Period Width"]]
data["Lower SMA Offset"] = data["Lower SMA Offset"][trading_settings["Start Interval"]:trading_settings["Start Interval"]+rollover_trading_settings["Trading Period Width"]]
data["High Crosses Upper Offset"] = data["High Crosses Upper Offset"][trading_settings["Start Interval"]:trading_settings["Start Interval"]+rollover_trading_settings["Trading Period Width"]]
data["Low Crosses Lower Offset"] = data["Low Crosses Lower Offset"][trading_settings["Start Interval"]:trading_settings["Start Interval"]+rollover_trading_settings["Trading Period Width"]]
data["Buy Signal"] = data["Buy Signal"][trading_settings["Start Interval"]:trading_settings["Start Interval"]+rollover_trading_settings["Trading Period Width"]]
data["Sell Signal"] = data["Sell Signal"][trading_settings["Start Interval"]:trading_settings["Start Interval"]+rollover_trading_settings["Trading Period Width"]]
account_value_array = account_value_array[trading_settings["Start Interval"]:trading_settings["Start Interval"]+rollover_trading_settings["Trading Period Width"]]
cash_value_array = cash_value_array[trading_settings["Start Interval"]:trading_settings["Start Interval"]+rollover_trading_settings["Trading Period Width"]]
position_value_array = position_value_array[trading_settings["Start Interval"]:trading_settings["Start Interval"]+rollover_trading_settings["Trading Period Width"]]
trading_interval_starting_values = { # This will be fed into walking forward values so we don't lose our cash and position values
"Cash Value": cash_value_array[-1],
"Position Value": position_value_array[-1],
"Account Value": cash_value_array[-1] + position_value_array[-1],
"Current Position": current_position
}
if current_position == 0:
trading_interval_starting_values["Current Trend"] = 0
else:
trading_interval_starting_values["Current Trend"] = int(current_position / abs(current_position)) # Bullish vs bearish
plottable_data["Open"] = data["Open"]
plottable_data["Close"] = data["Close"]
plottable_data["High"] = data["High"]
plottable_data["Low"] = data["Low"]
plottable_data["Short SMA Length"].extend([best_algo_settings[0] for i in range(rollover_trading_settings["Trading Period Width"])])
plottable_data["Long SMA Length"].extend([best_algo_settings[1] for i in range(rollover_trading_settings["Trading Period Width"])])
plottable_data["Offset Percent"].extend([best_algo_settings[2] for i in range(rollover_trading_settings["Trading Period Width"])])
plottable_data["Short SMA"].extend(data["Short SMA"])
plottable_data["Long SMA"].extend(data["Long SMA"])
plottable_data["Upper Offset"].extend(data["Upper SMA Offset"])
plottable_data["Lower Offset"].extend(data["Lower SMA Offset"])
plottable_data["Buy Signal"].extend(data["Buy Signal"])
plottable_data["Sell Signal"].extend(data["Sell Signal"])
plottable_data["Account Value"].extend(account_value_array)
plottable_data["Cash Value"].extend(cash_value_array)
plottable_data["Position Value"].extend(position_value_array)
plottable_data["Trade Period Marker"][trading_settings["Start Interval"]] = 1
print(f'After Trading Interval {i}. Start Training Interval: {optimization_settings["Start Interval"]}, Stop Training Interval: {optimization_settings["Start Interval"] + optimization_settings["Intervals in Period"]}')
print(f'Start Trading Interval: {trading_settings["Start Interval"]}, Stop Trading Interval: {trading_settings["Start Interval"]+trading_settings["Intervals in Period"]}')
print(f'Algorithm Settings for Interval: {best_algo_settings}')
print(f'Account value: ${account_value}')
print('\n')
trading_settings["Starting Value"] = account_value # Basically so we can reinvest
optimization_settings["Start Interval"] += rollover_trading_settings["Trading Period Width"] # Update where we want to begin the training for the next training period
trading_settings["Start Interval"] += rollover_trading_settings["Trading Period Width"] # Update where we want to begin the trading for the next trading period
if rollover_trading_settings["Just Final"] == 0: # We want to return all of the data to plot
return plottable_data
def main(stock_settings={}, trading_settings={}, optimization_settings={}, rollover_trading_settings={}, action=1):
'''action: 1 is just use trading_settings to show what would have happened using those value.
2 is using optimization_settings to optimize then show what would have happend using the best values.
3 is a more realistic option using rollover_trading_settings to first train on past data then apply the parameters to data not optimized for.
'''
if action == 1:
data = read_ticker_data(stock_settings, trading_settings)
# Do some quick number checking to make sure things size up
if len(data["Close"]) < trading_settings["Start Interval"]+trading_settings["Intervals in Period"]:
print("Start Trade Interval + Number of Trade Intervals is more than the amount of data we have")
return
if trading_settings["Intervals in Period"] == 0: # This is to tell us that we want to use all data available to simulate trading
trading_settings["Intervals in Period"] = len(data["Close"])-trading_settings["Start Interval"]
data["Short SMA"] = generate_sma(data["Close"], trading_settings["Short SMA"], trading_settings)
data["Long SMA"] = generate_sma(data["Close"], trading_settings["Long SMA"], trading_settings)
data["Upper SMA Offset"], data["Lower SMA Offset"] = generate_offset(data, trading_settings["Offset Percent"], ["Short SMA", "Long SMA"], trading_settings)
data["High Crosses Upper Offset"] = crossover_events(data, ["Close", "Upper SMA Offset"], trading_settings)[0]
data["Low Crosses Lower Offset"] = crossover_events(data, ["Close", "Lower SMA Offset"], trading_settings)[1]
data["Buy Signal"] = trend_reversal_event(data, 1, trading_settings)[0]
data["Sell Signal"] = trend_reversal_event(data, 1, trading_settings)[1]
data["Account Value"], data["Cash Value"], data["Position Value"] = walking_forward_value(data, 0, trading_settings)
data["Final Account Value"] = walking_forward_value(data, 1, trading_settings)
title = f'{stock_settings["Ticker"]} {stock_settings["Period"]} {stock_settings["Interval"]} | Trading from {optimization_settings["Start Interval"]} to {optimization_settings["Intervals in Period"]-1} | SSMA({trading_settings["Short SMA"]}) LSMA({trading_settings["Long SMA"]}) OP({trading_settings["Offset Percent"]}%) ID({trading_settings["Interval Delay"]})'
plot_values_stacked(data, [["Close", "High", "Low", "Upper SMA Offset", "Lower SMA Offset"], ["Buy Signal", "Sell Signal"], ["Account Value", "Position Value", "Cash Value"]], [["black", "orange", "blue", "grey", "grey"], ["green", "red"], ["black", "blue", "grey"]], title)
print(f"Started with ${trading_settings['Starting Value']}, finished with ${data['Final Account Value']}, {round((data['Final Account Value'] / trading_settings['Starting Value']-1)*100, 3)}% increase.\n")
elif action == 2:# Here we will run an optimizer to get the best settings
data = read_ticker_data(stock_settings, optimization_settings)
# Do some quick number checking to make sure things size up
if len(data["Close"]) < optimization_settings["Start Interval"]+optimization_settings["Intervals in Period"]:
print("Start Optimization Interval + Number of Intervals is more than the amount of data we have")
return
if optimization_settings["Intervals in Period"] == 0: # This is to tell us that we want to use all data available to simulate trading
optimization_settings["Intervals in Period"] = len(data["Close"])-optimization_settings["Start Interval"]
best_algo_settings, best_account_increase_percent = optimize_algo_settings(data, optimization_settings)
data["Short SMA"] = generate_sma(data["Close"], best_algo_settings[0], optimization_settings)
data["Long SMA"] = generate_sma(data["Close"], best_algo_settings[1], optimization_settings)
data["Upper SMA Offset"], data["Lower SMA Offset"] = generate_offset(data, best_algo_settings[2], ["Short SMA", "Long SMA"], optimization_settings)
data["High Crosses Upper Offset"] = crossover_events(data, ["Close", "Upper SMA Offset"], optimization_settings)[0]
data["Low Crosses Lower Offset"] = crossover_events(data, ["Close", "Lower SMA Offset"], optimization_settings)[1]
data["Buy Signal"] = trend_reversal_event(data, 1, optimization_settings)[0]
data["Sell Signal"] = trend_reversal_event(data, 1, optimization_settings)[1]
data["Account Value"], data["Cash Value"], data["Position Value"] = walking_forward_value(data, 0, optimization_settings)
data["Final Account Value"] = walking_forward_value(data, 1, optimization_settings)
title = f'{stock_settings["Ticker"]} {stock_settings["Period"]} {stock_settings["Interval"]} | Optimizing from {optimization_settings["Start Interval"]} to {optimization_settings["Intervals in Period"]-1} | SSMA Range({optimization_settings["Short SMA Range"]}) LSMA Range({optimization_settings["Long SMA Range"]}) OP Range({optimization_settings["Offset Percent Range"]}, {optimization_settings["Offset Percent Step"]}%) | SSMA({best_algo_settings[0]}) LSMA({best_algo_settings[1]}) OP({best_algo_settings[2]}%) ID({optimization_settings["Interval Delay"]})'
plot_values_stacked(data, [["Close", "High", "Low", "Upper SMA Offset", "Lower SMA Offset"], ["Buy Signal", "Sell Signal"], ["Account Value", "Position Value", "Cash Value"]], [["black", "orange", "blue", "grey", "grey"], ["green", "red"], ["black", "blue", "grey"]], title)
print(f"Best algorithm settings: {best_algo_settings}")
print(f"{best_account_increase_percent} % increase.\n")
elif action == 3: # Rollover trading
if rollover_trading_settings["Just Final"]:
rolling_optimization_then_trade(stock_settings, trading_settings, optimization_settings, rollover_trading_settings)
else:
plottable_data = rolling_optimization_then_trade(stock_settings, trading_settings, optimization_settings, rollover_trading_settings)
title = f'{stock_settings["Ticker"]} {stock_settings["Period"]} {stock_settings["Interval"]} | Rollover Trading from {rollover_trading_settings["Start Interval"]} to {len(plottable_data["Account Value"])-1} | Training Width({rollover_trading_settings["Training Period Width"]}) Trading Width({rollover_trading_settings["Trading Period Width"]}) | WC({optimization_settings["Worst Case"]}) ID({optimization_settings["Interval Delay"]}) MIBT({optimization_settings["Minimum Intervals Per Trade"]})'
plot_values_stacked(plottable_data, [["Close", "Open", "High", "Low", "Upper Offset", "Lower Offset"], ["Short SMA Length", "Long SMA Length", "Offset Percent"], ["Buy Signal", "Sell Signal", "Trade Period Marker"], ["Account Value", "Position Value", "Cash Value"]], [["black", "white", "orange", "blue", "grey", "grey"], ["blue", "orange", "black"], ["green", "red", "black"], ["black", "blue", "grey"]], title)
######################################################################################################################################################################################################
stock_settings = {
"Ticker": "NVDA",
"Period": "720d",
"Interval": "15m"
}
trading_settings = {
"Start Interval": 0,
"Intervals in Period": 0, # Use 0 to use all available data
"Short SMA": 12,
"Long SMA": 72,
"Offset Percent": 2.1,
"Interval Delay": 1,
"Reinvest": 1,
"Worst Case": 0,
"Starting Value": 1000
}
optimization_settings = {
"Start Interval": 0,
"Intervals in Period": 0, # Use 0 to use all available data
"Short SMA Range": [1, 15],
"Short SMA Step": 1,
"Long SMA Range": [2, 75],
"Long SMA Step": 1,
"Offset Percent Range": [0.25, 5],
"Offset Percent Step": 0.25,
"Optimization Combinations": [], # Used for multithreading, it is an array of every possible combination to test then report back the best.
"Interval Delay": 1,
"Reinvest": 1,
"Worst Case": 0,
"Minimum Intervals Per Trade": 112, # 7 hours per trading day, 14 30m intervals, 28 15min intervals, 84 5min intervals
"Starting Value": 10000, # Only for optimization algorithm
"Multithreaded Training": 1, # Should we use multithreading
"Pools": 40, # How many pools to use for multithreading
"Graph Optimizations": 0 # Whether or not we should graph a plot of the different optimization values
}
rollover_trading_settings = {
"Start Interval": 0,
"Intervals to Test": 0, # How many data points from our available data to use. Use 0 to use all available data
"Training Period Width": 600, # 7 hours per trading day, 1 week = 140, 1 month = 600, 6 months = 2400 ***Rough numbers
"Trading Period Width": 280,
"Starting Value": 1000, # This is used to actually trade during our trading interval. The Starting Value under optimization settings is used just for it to get the best parameters
"Just Final": 0 # If we want to be able to plot or just want the final value
}
#import cProfile
#cProfile.run('main(stock_settings, trading_settings, optimization_settings, rollover_trading_settings, 3)')
if __name__ == '__main__':
start_time = time.time()
main(stock_settings, trading_settings, optimization_settings, rollover_trading_settings, 3)
print("--- %s seconds ---" % (time.time() - start_time))