#These settings can be configured in a global config.ini in the program root directory under [Settings]
useWebProxyServer = False	#If you need a web proxy to browse the web
nonGUIEnvironment = False	#hosted environments often have no GUI so matplotlib won't be outputting to display
suspendPriceLoads = False

#pip install any of these if they are missing
import time, datetime, random, os, ssl, matplotlib
import numpy as np,  pandas as pd
from pandas.tseries.offsets import BDay
import urllib.request as webRequest
from _classes.Utility import *

#pricingData and TradingModel are the two intended exportable classes
#user input dates are expected to be in local format, after that they should be in database format
#-------------------------------------------- Global settings -----------------------------------------------
nonGUIEnvironment = ReadConfigBool('Settings', 'nonGUIEnvironment')
if nonGUIEnvironment: matplotlib.use('agg',warn=False, force=True)
from matplotlib import pyplot as plt
import matplotlib.dates as mdates
currentProxyServer = None
proxyList = ['173.232.228.25:8080']
useWebProxyServer = ReadConfigBool('Settings', 'useWebProxyServer')
if useWebProxyServer: 
	x =  ReadConfigList('Settings', 'proxyList')
	if not x == None: proxyList = x		

BaseFieldList = ['Open','Close','High','Low']
#-------------------------------------------- General Utilities -----------------------------------------------
def PandaIsInIndex(df:pd.DataFrame, value):
	try:
		x = df.loc[value]
		r = True
	except:
		r = False
	return r

#datetime.datetime.fromtimestamp(dt).date()
def GetMyDateFormat(): return '%m/%d/%Y'

def DateFormatDatabase(givenDate:datetime):
#returns datetime object
	if type(givenDate) == str:
		if givenDate.find('-') > 0 :
			r = datetime.datetime.strptime(givenDate, '%Y-%m-%d')
		else:
			r = datetime.datetime.strptime(givenDate, GetMyDateFormat())
	elif type(givenDate) == datetime:
		r = datetime.datetime.fromtimestamp(givenDate).date()
	else:
		r = givenDate
	return r

def GetDateTimeStamp():
	d = datetime.datetime.now()
	return d.strftime('%Y%m%d%H%M')

def GetTodaysDate():
	d = datetime.datetime.now()
	return d.strftime('%m/%d/%Y')

def DateDiffDays(startDate:datetime, endDate:datetime):
	delta = endDate-startDate
	return delta.days

def DateDiffHours(startDate:datetime, endDate:datetime):
	delta = endDate-startDate
	return int(delta.total_seconds() / 3600)

def CreateFolder(p:str):
	r = True
	if not os.path.exists(p):
		try:
			os.mkdir(p)	
		except Exception as e:
			print('Unable to create folder: ' + p)
			f = False
	return r
	
def PlotInitDefaults():
	#'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'
	params = {'legend.fontsize': 'xx-small', 'axes.labelsize': 'xx-small','axes.titlesize':'xx-small','xtick.labelsize':'xx-small','ytick.labelsize':'xx-small'}
	plt.rcParams.update(params)

def PlotScalerDateAdjust(minDate:datetime, maxDate:datetime, ax):
	if type(minDate)==str:
		daysInGraph = DateDiffDays(minDate,maxDate)
	else:
		daysInGraph = (maxDate-minDate).days
	if daysInGraph >= 365*3:
		majorlocator =  mdates.YearLocator()
		minorLocator = mdates.MonthLocator()
		majorFormatter = mdates.DateFormatter('%m/%d/%Y')
	elif daysInGraph >= 365:
		majorlocator =  mdates.MonthLocator()
		minorLocator = mdates.WeekdayLocator()
		majorFormatter = mdates.DateFormatter('%m/%d/%Y')
	elif daysInGraph < 90:
		majorlocator =  mdates.DayLocator()
		minorLocator = mdates.DayLocator()
		majorFormatter =  mdates.DateFormatter('%m/%d/%Y')
	else:
		majorlocator =  mdates.WeekdayLocator()
		minorLocator = mdates.DayLocator()
		majorFormatter =  mdates.DateFormatter('%m/%d/%Y')
	ax.xaxis.set_major_locator(majorlocator)
	ax.xaxis.set_major_formatter(majorFormatter)
	ax.xaxis.set_minor_locator(minorLocator)
	#ax.xaxis.set_minor_formatter(daysFmt)
	ax.set_xlim(minDate, maxDate)

def PlotDataFrame(df:pd.DataFrame, title:str, xlabel:str, ylabel:str, adjustScale:bool=True, fileName:str = '', dpi:int = 600):
	if df.shape[0] >= 4:
		PlotInitDefaults()
		ax=df.plot(title=title, linewidth=.75)
		ax.set_xlabel(xlabel)
		ax.set_ylabel(ylabel)
		ax.tick_params(axis='x', rotation=70)
		ax.grid(b=True, which='major', color='black', linestyle='solid', linewidth=.5)
		ax.grid(b=True, which='minor', color='0.65', linestyle='solid', linewidth=.3)
		if adjustScale:
			dates= df.index.get_level_values('Date')
			minDate = dates.min()
			maxDate = dates.max()
			PlotScalerDateAdjust(minDate, maxDate, ax)
		if not fileName =='':
			if not fileName[-4] == '.': fileName+= '.png'
			plt.savefig(fileName, dpi=dpi)			
		else:
			fig = plt.figure(1)
			fig.canvas.set_window_title(title)
			plt.show()
		plt.close('all')

def GetProxiedOpener():
	testURL = 'https://stooq.com'
	userName, password = 'mUser', 'SecureAccess'
	context = ssl._create_unverified_context()
	handler = webRequest.HTTPSHandler(context=context)
	i = -1
	functioning = False
	global currentProxyServer
	while not functioning and i < len(proxyList):
		if i >=0 or currentProxyServer==None: currentProxyServer = proxyList[i]
		proxy = webRequest.ProxyHandler({'https': r'http://' + userName + ':' + password + '@' + currentProxyServer})
		auth = webRequest.HTTPBasicAuthHandler()
		opener = webRequest.build_opener(proxy, auth, handler) 
		opener.addheaders = [('User-agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.1 Safari/603.1.30')]
		#opener.addheaders = [('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Safari/605.1.15')]	
		#opener.addheaders = [('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763')]
		try:
			conn = opener.open(testURL)
			print('Proxy ' + currentProxyServer + ' is functioning')
			functioning = True
		except:
			print('Proxy ' + currentProxyServer + ' is not responding')
		i+=1
	return opener

#-------------------------------------------- Classes -----------------------------------------------
class PlotHelper:
	def PlotDataFrame(self, df:pd.DataFrame, title:str='', xlabel:str='', ylabel:str='', adjustScale:bool=True, fileName:str = '', dpi:int=600): PlotDataFrame(df, title, xlabel, ylabel, adjustScale, fileName, dpi)

	def PlotDataFrameDateRange(self, df:pd.DataFrame, endDate:datetime=None, historyDays:int=90, title:str='', xlabel:str='', ylabel:str='', fileName:str = '', dpi:int=600):
		if df.shape[0] > 10: 
			if endDate==None: endDate=df.index[-1] 	#The latest date in the dataframe assuming ascending order				
			endDate = DateFormatDatabase(endDate)
			startDate = endDate - BDay(historyDays)
			df = df[df.index >= startDate]
			df = df[df.index <= endDate]
			PlotDataFrame(df, title, xlabel, ylabel, True, fileName, dpi)
	
class PriceSnapshot:
	ticker=''
	high=0
	low=0
	open=0
	close=0
	oneDayAverage=0
	twoDayAverage=0
	fiveDayAverage=0
	shortEMA=0
	shortEMASlope=0
	longEMA=0
	longEMASlope=0
	channelHigh=0
	channelLow=0
	oneDayApc = 0
	oneDayDeviation=0
	fiveDayDeviation=0
	fifteenDayDeviation=0
	dailyGain=0
	monthlyGain=0
	monthlyLossStd=0
	estLow=0
	nextDayTarget=0
	estHigh=0
	snapShotDate=None
	
class PricingData:
	#Historical prices for a given stock, along with statistics, and future estimated prices
	stockTicker = ''
	historicalPrices = None	#dataframe with price history indexed on date
	pricePredictions = None #dataframe with price predictions indexed on date
	historyStartDate = None
	historyEndDate = None
	pricesLoaded = False
	statsLoaded = False
	predictionsLoaded = False
	predictionDeviation = 0	#Average percentage off from target
	pricesNormalized = False
	pricesInPercentages = False
	_dataFolderhistoricalPrices = 'data/historical/'
	_dataFolderCharts = 'data/charts/'
	_dataFolderDailyPicks = 'data/dailypicks/'
	
	def __init__(self, ticker:str, dataFolderRoot:str=''):
		self.stockTicker = ticker
		if not dataFolderRoot =='':
			if CreateFolder(dataFolderRoot):
				if not dataFolderRoot[-1] =='/': dataFolderRoot += '/'
				self._dataFolderCharts = dataFolderRoot + 'charts/'
				self._dataFolderhistoricalPrices = dataFolderRoot + 'historical/'
				self._dataFolderDailyPicks = dataFolderRoot + 'dailypicks/'
		else: CreateFolder('data')
		CreateFolder(self._dataFolderhistoricalPrices)
		CreateFolder(self._dataFolderCharts)
		CreateFolder(self._dataFolderDailyPicks)
		self.pricesLoaded = False
		self.statsLoaded = False
		self.predictionsLoaded = False
		self.historicalPrices = None	
		self.pricePredictions = None
		self.historyStartDate = None
		self.historyEndDate = None

	def __del__(self):
		self.pricesLoaded = False
		self.statsLoaded = False
		self.predictionsLoaded = False
		self.historicalPrices = None	
		self.pricePredictions = None
		self.historyStartDate = None
		self.historyEndDate = None

	def PrintStatus(self):
		print("pricesLoaded=" + str(self.pricesLoaded))
		print("statsLoaded=" + str(self.statsLoaded))
		print("historyStartDate=" + str(self.historyStartDate))
		print("historyEndDate=" + str(self.historyEndDate))
		
	def _DownloadPriceData(self,verbose:bool=False):
		url = "https://stooq.com/q/d/l/?i=d&s=" + self.stockTicker + '.us'
		if self.stockTicker[0] == '^': url = "https://stooq.com/q/d/l/?i=d&s=" + self.stockTicker 
		filePath = self._dataFolderhistoricalPrices + self.stockTicker + '.csv'
		s1 = ''
		if CreateFolder(self._dataFolderhistoricalPrices): filePath = self._dataFolderhistoricalPrices + self.stockTicker + '.csv'
		try:
			if useWebProxyServer:
				opener = GetProxiedOpener()
				openUrl = opener.open(url)
			else:
				openUrl = webRequest.urlopen(url) 
			r = openUrl.read()
			openUrl.close()
			s1 = r.decode()
			s1 = s1.replace(chr(13),'')
		except Exception as e:
			if verbose: print('Web connection error: ', e)
		if len(s1) < 1024:
			if verbose: print(' No data found online for ticker ' + self.stockTicker, url)
			if useWebProxyServer:
				global currentProxyServer
				global proxyList
				if not currentProxyServer==None and len(proxyList) > 3: 
					if verbose: print( 'Removing proxy: ', currentProxyServer)
					proxyList.remove(currentProxyServer)
					currentProxyServer = None
		else:
			if verbose: print(' Downloaded new data for ticker ' + self.stockTicker)
			f = open(filePath,'w')
			f.write(s1)
			f.close()

	def _LoadHistory(self, refreshPrices:bool=False,verbose:bool=False):
		filePath = self._dataFolderhistoricalPrices + self.stockTicker + '.csv'
		if not os.path.isfile(filePath) or refreshPrices: self._DownloadPriceData(verbose=verbose)

		if not os.path.isfile(filePath):
			print(' No data found for ' + self.stockTicker)
			self.pricesLoaded = False
		else:
			df = pd.read_csv(filePath, index_col=0, parse_dates=True, na_values=['nan'])
			df = df[BaseFieldList]
			df['Average'] = df.loc[:,BaseFieldList].mean(axis=1) #select those rows, calculate the mean value
			if (df['Open'] < df['Low']).any() or (df['Close'] < df['Low']).any() or (df['High'] < df['Low']).any() or (df['Open'] > df['High']).any() or (df['Close'] > df['High']).any(): 
				if verbose:
					print(self.stockTicker)
					print(df.loc[df['Low'] > df['High']])
					print(' Data validation error, Low > High.  Dropping values..')
				df = df.loc[df['Low'] <= df['High']]
				df = df.loc[df['Low'] <= df['Open']]
				df = df.loc[df['Low'] <= df['Close']]
				df = df.loc[df['High'] >= df['Open']]
				df = df.loc[df['High'] >= df['Close']]
			self.historicalPrices = df
			self.historyStartDate = self.historicalPrices.index.min()
			self.historyEndDate = self.historicalPrices.index.max()
			self.pricesLoaded = True
		return self.pricesLoaded 
		
	def LoadHistory(self, requestedStartDate:datetime=None, requestedEndDate:datetime=None, verbose:bool=False):
		self._LoadHistory(refreshPrices=False, verbose=verbose)
		if self.pricesLoaded:
			requestNewData = False
			filePath = self._dataFolderhistoricalPrices + self.stockTicker + '.csv'
			lastUpdated = datetime.datetime.fromtimestamp(os.path.getmtime(filePath))
			if DateDiffHours(lastUpdated, datetime.datetime.now()) > 12 and not suspendPriceLoads:	#Limit how many times per hour we refresh the data to prevent abusing the source
				if not(requestedStartDate==None): requestNewData = (requestedStartDate < self.historyStartDate)
				if not(requestedEndDate==None): requestNewData = (requestNewData or (self.historyEndDate < requestedEndDate))
				if requestNewData and verbose: print(' Requesting new data for ' + self.stockTicker + ' (requestedStart, historyStart, historyEnd, requestedEnd)', requestedStartDate, self.historyStartDate, self.historyEndDate, requestedEndDate)
				if (requestedStartDate==None and requestedEndDate==None):
					requestNewData = (DateDiffDays(startDate=lastUpdated, endDate=datetime.datetime.now()) > 1)
				if verbose: print(' Requesting new data ' + self.stockTicker + ' reason stale data ', lastUpdated)
			if requestNewData: self._LoadHistory(refreshPrices=True, verbose=verbose)
		return(self.pricesLoaded)

	def TrimToDateRange(self,startDate:datetime, endDate:datetime):
		startDate = DateFormatDatabase(startDate)
		endDate = DateFormatDatabase(endDate)
		self.historicalPrices = self.historicalPrices[self.historicalPrices.index >= startDate]
		self.historicalPrices = self.historicalPrices[self.historicalPrices.index <= endDate]
		self.historyStartDate = self.historicalPrices.index.min()
		self.historyEndDate = self.historicalPrices.index.max()
	
	def ConvertToPercentages(self):
		if self.pricesInPercentages:
			self.historicalPrices.iloc[0] = self.CTPFactor
			for i in range(1, self.historicalPrices.shape[0]):
				self.historicalPrices.iloc[i] = (1 + self.historicalPrices.iloc[i]) * self.historicalPrices.iloc[i-1]
			if self.predictionsLoaded:
				self.pricePredictions.iloc[0] = self.CTPFactor['Average']
				for i in range(1, self.pricePredictions.shape[0]):
					self.pricePredictions.iloc[i] = (1 + self.pricePredictions.iloc[i]) * self.pricePredictions.iloc[i-1]
			self.pricesInPercentages = False
			print(' Prices have been converted back from percentages.')
		else:
			self.CTPFactor = self.historicalPrices.iloc[0]
			self.historicalPrices = self.historicalPrices[['Open','Close','High','Low','Average']].pct_change(1)
			self.historicalPrices[:1] = 0
			if self.predictionsLoaded:
				self.pricePredictions = self.pricePredictions.pct_change(1)
			self.statsLoaded = False
			self.pricesInPercentages = True
			print(' Prices have been converted to percentage change from previous day.')
		
	def NormalizePrices(self, verbose:bool=False):
		#(x-min(x))/(max(x)-min(x))
		x = self.historicalPrices
		if not self.pricesNormalized:
			low = x['Low'].min(axis=0) - .000001 #To prevent div zero calculation errors.
			high = x['High'].max(axis=0)
			diff = high-low
			x['Open'] = (x['Open']-low)/diff
			x['Close'] = (x['Close']-low)/diff
			x['High'] = (x['High']-low)/diff
			x['Low'] = (x['Low']-low)/diff
			if self.predictionsLoaded:
				self.pricePredictions['estLow'] = (self.pricePredictions['estLow']-low)/diff
				self.pricePredictions['estAverage'] = (self.pricePredictions['estAverage']-low)/diff
				self.pricePredictions['estHigh'] = (self.pricePredictions['estHigh']-low)/diff
			self.PreNormalizationLow = low
			self.PreNormalizationHigh = high
			self.PreNormalizationDiff = diff
			self.pricesNormalized = True
			print(' Prices have been normalized.')
		else:
			low = self.PreNormalizationLow
			high = self.PreNormalizationHigh 
			diff = self.PreNormalizationDiff
			x['Open'] = (x['Open'] * diff) + low
			x['Close'] = (x['Close'] * diff) + low
			x['High'] = (x['High'] * diff) + low
			x['Low'] = (x['Low'] * diff) + low
			if self.predictionsLoaded:
				self.pricePredictions['estLow'] = (self.pricePredictions['estLow'] * diff) + low
				self.pricePredictions['estAverage'] = (self.pricePredictions['estAverage'] * diff) + low
				self.pricePredictions['estHigh'] = (self.pricePredictions['estHigh'] * diff) + low
			self.pricesNormalized = False
			print(' Prices have been un-normalized.')
		x['Average'] = (x['Open'] + x['Close'] + x['High'] + x['Low'])/4
		#x['Average'] = x.loc[:,BaseFieldList].mean(axis=1, skipna=True) #Wow, this doesn't work.
		if (x['Average'] < x['Low']).any() or (x['Average'] > x['High']).any(): 
			print(x.loc[x['Average'] < x['Low']])
			print(x.loc[x['Average'] > x['High']])
			print(x.loc[x['Low'] > x['High']])
			print(self.PreNormalizationLow, self.PreNormalizationHigh, self.PreNormalizationDiff, self.PreNormalizationHigh-self.PreNormalizationLow)
			#print(x.loc[:,BaseFieldList].mean(axis=1))
			print('Stop: averages not computed correctly.')
			assert(False)
		self.historicalPrices = x
		if self.statsLoaded: self.CalculateStats()
		if verbose: print(self.historicalPrices[:1])

	def CalculateStats(self):
		if not self.pricesLoaded: self.LoadHistory()
		self.historicalPrices['2DayAv'] = self.historicalPrices['Average'].rolling(window=2, center=False).mean()
		self.historicalPrices['5DayAv'] = self.historicalPrices['Average'].rolling(window=5, center=False).mean()
		self.historicalPrices['shortEMA'] =  self.historicalPrices['Average'].ewm(com=3,min_periods=0,adjust=True,ignore_na=False).mean()
		self.historicalPrices['shortEMASlope'] = (self.historicalPrices['shortEMA']/self.historicalPrices['shortEMA'].shift(1))-1
		self.historicalPrices['longEMA'] = self.historicalPrices['Average'].ewm(com=9,min_periods=0,adjust=True,ignore_na=False).mean()
		self.historicalPrices['longEMASlope'] = (self.historicalPrices['longEMA']/self.historicalPrices['longEMA'].shift(1))-1
		self.historicalPrices['45dEMA'] = self.historicalPrices['Average'].ewm(com=22,min_periods=0,adjust=True,ignore_na=False).mean()
		self.historicalPrices['45dEMASlope'] = (self.historicalPrices['45dEMA']/self.historicalPrices['45dEMA'].shift(1))-1
		self.historicalPrices['1DayDeviation'] = (self.historicalPrices['High'] - self.historicalPrices['Low'])/self.historicalPrices['Low']
		self.historicalPrices['5DavDeviation'] = self.historicalPrices['1DayDeviation'].rolling(window=5, center=False).mean()
		self.historicalPrices['15DavDeviation'] = self.historicalPrices['1DayDeviation'].rolling(window=15, center=False).mean()
		self.historicalPrices['1DayApc'] = ((self.historicalPrices['Average'] - self.historicalPrices['Average'].shift(1)) / self.historicalPrices['Average'].shift(1))
		self.historicalPrices['3DayApc'] = self.historicalPrices['1DayApc'].rolling(window=3, center=False).mean()
		self.historicalPrices['dailyGain'] = (self.historicalPrices['5DayAv'] / self.historicalPrices['5DayAv'].shift(1))-1
		self.historicalPrices['monthlyGain'] = (self.historicalPrices['5DayAv'] / self.historicalPrices['5DayAv'].shift(20))-1
		#self.historicalPrices['monthlyGainStd'] = self.historicalPrices['monthlyGain'].rolling(window=253, center=False).std()
		self.historicalPrices['monthlyLosses'] = self.historicalPrices['monthlyGain']
		self.historicalPrices['monthlyLosses'].loc[self.historicalPrices['monthlyLosses'] > 0] = 0 #zero out the positives
		self.historicalPrices['monthlyLossStd'] = self.historicalPrices['monthlyLosses'].rolling(window=253, center=False).std()	#Stdev of negative values, these are the negative monthly price drops in the past year
		self.historicalPrices['1DayMomentum'] = (self.historicalPrices['Average'] / self.historicalPrices['Average'].shift(1))-1
		self.historicalPrices['3DayMomentum'] = (self.historicalPrices['Average'] / self.historicalPrices['Average'].shift(3))-1
		self.historicalPrices['5DayMomentum'] = (self.historicalPrices['Average'] / self.historicalPrices['Average'].shift(5))-1
		self.historicalPrices['10DayMomentum'] = (self.historicalPrices['Average'] / self.historicalPrices['Average'].shift(10))-1
		self.historicalPrices['channelHigh'] = self.historicalPrices['longEMA'] + (self.historicalPrices['Average']*self.historicalPrices['15DavDeviation'])
		self.historicalPrices['channelLow'] = self.historicalPrices['longEMA'] - (self.historicalPrices['Average']*self.historicalPrices['15DavDeviation'])
		self.historicalPrices.fillna(method='ffill', inplace=True)
		self.historicalPrices.fillna(method='bfill', inplace=True)
		self.statsLoaded = True
		return True

	def MonthyReturnVolatility(self): return self.historicalPrices['MonthlyGain'].rolling(window=253, center=False).std() #of the past year

	def SaveStatsToFile(self, includePredictions:bool=False, verbose:bool=False):
		if includePredictions:
			filePath = self._dataFolderhistoricalPrices + self.stockTicker + '_stats_predictions.csv'
			r = self.historicalPrices.join(self.pricePredictions, how='outer') #, rsuffix='_Predicted'
			r.to_csv(filePath)
		else:
			filePath = self._dataFolderhistoricalPrices + self.stockTicker + '_stats.csv'
			self.historicalPrices.to_csv(filePath)
		print('Statistics saved to: ' + filePath)
		
	def PredictPrices(self, method:int=1, daysIntoFuture:int=1, NNTrainingEpochs:int=0):
		#Predict current prices from previous days info
		self.predictionsLoaded = False
		self.pricePredictions = pd.DataFrame()	#Clear any previous data
		if not self.statsLoaded: self.CalculateStats()
		if method < 3:
			minActionableSlope = 0.001
			if method==0:	#Same as previous day
				self.pricePredictions = pd.DataFrame()
				self.pricePredictions['estLow'] =  self.historicalPrices['Low'].shift(1)
				self.pricePredictions['estHigh'] = self.historicalPrices['High'].shift(1)
			elif method==1 :	#Slope plus momentum with some consideration for trend.
					#++,+-,-+,==
				bucket = self.historicalPrices.copy()
				bucket['estLow']  = bucket['Average'].shift(1) * (1-bucket['15DavDeviation']/2) + (abs(bucket['1DayApc'].shift(1)))
				bucket['estHigh'] = bucket['Average'].shift(1) * (1+bucket['15DavDeviation']/2) + (abs(bucket['1DayApc'].shift(1)))
				bucket = bucket.query('longEMASlope >= -' + str(minActionableSlope) + ' or shortEMASlope >= -' + str(minActionableSlope)) #must filter after rolling calcuations
				bucket = bucket[['estLow','estHigh']]
				self.pricePredictions = bucket
					#-- downward trend
				bucket = self.historicalPrices.copy()
				bucket['estLow'] = bucket['Low'].shift(1).rolling(3).min() * .99
				bucket['estHigh'] = bucket['High'].shift(1).rolling(3).min() 
				bucket = bucket.query('not (longEMASlope >= -' + str(minActionableSlope) + ' or shortEMASlope >= -' + str(minActionableSlope) +')')
				bucket = bucket[['estLow','estHigh']]
				self.pricePredictions = self.pricePredictions.append(bucket)
				self.pricePredictions.sort_index(inplace=True)	
			elif method==2:	#Slope plus momentum with full consideration for trend.
					#++ Often over bought, strong momentum
				bucket = self.historicalPrices.copy() 
				#bucket['estLow']  = bucket['Low'].shift(1).rolling(4).max()  * (1 + abs(bucket['shortEMASlope'].shift(1)))
				#bucket['estHigh'] = bucket['High'].shift(1).rolling(4).max()  * (1 + abs(bucket['shortEMASlope'].shift(1)))
				bucket['estLow']  = bucket['Low'].shift(1).rolling(4).max()  + (abs(bucket['1DayApc'].shift(1)))
				bucket['estHigh'] = bucket['High'].shift(1).rolling(4).max() + (abs(bucket['1DayApc'].shift(1)))
				bucket = bucket.query('longEMASlope >= ' + str(minActionableSlope) + ' and shortEMASlope >= ' + str(minActionableSlope)) #must filter after rolling calcuations
				bucket = bucket[['estLow','estHigh']]
				self.pricePredictions = bucket
					#+- correction or early down turn, loss of momentum
				bucket = self.historicalPrices.copy()
				bucket['estLow']  = bucket['Low'].shift(1).rolling(2).min() 
				bucket['estHigh'] = bucket['High'].shift(1).rolling(3).max()  * (1.005 + abs(bucket['shortEMASlope'].shift(1)))
				bucket = bucket.query('longEMASlope >= ' + str(minActionableSlope) + ' and shortEMASlope < -' + str(minActionableSlope))
				bucket = bucket[['estLow','estHigh']]
				self.pricePredictions = self.pricePredictions.append(bucket)
					 #-+ bounce or early recovery, loss of momentum
				bucket = self.historicalPrices.copy()
				bucket['estLow']  = bucket['Low'].shift(1)
				bucket['estHigh'] = bucket['High'].shift(1).rolling(3).max() * 1.02 
				bucket = bucket.query('longEMASlope < -' + str(minActionableSlope) + ' and shortEMASlope >= ' + str(minActionableSlope))
				bucket = bucket[['estLow','estHigh']]
					#-- Often over sold
				self.pricePredictions = self.pricePredictions.append(bucket)
				bucket = self.historicalPrices.copy() 
				bucket['estLow'] = bucket['Low'].shift(1).rolling(3).min() * .99
				bucket['estHigh'] = bucket['High'].shift(1).rolling(3).min() 
				bucket = bucket.query('longEMASlope < -' + str(minActionableSlope) + ' and shortEMASlope < -' + str(minActionableSlope))
				bucket = bucket[['estLow','estHigh']]
				self.pricePredictions = self.pricePredictions.append(bucket)
					#== no significant slope
				bucket = self.historicalPrices.copy() 
				bucket['estLow']  = bucket['Low'].shift(1).rolling(4).mean()
				bucket['estHigh'] = bucket['High'].shift(1).rolling(4).mean()
				bucket = bucket.query(str(minActionableSlope) + ' > longEMASlope >= -' + str(minActionableSlope) + ' or ' + str(minActionableSlope) + ' > shortEMASlope >= -' + str(minActionableSlope))
				bucket = bucket[['estLow','estHigh']]
				self.pricePredictions = self.pricePredictions.append(bucket)
				self.pricePredictions.sort_index(inplace=True)	
			d = self.historicalPrices.index[-1] 
			ls = self.historicalPrices['longEMASlope'][-1]
			ss = self.historicalPrices['shortEMASlope'][-1]
			deviation = self.historicalPrices['15DavDeviation'][-1]/2
			momentum = self.historicalPrices['3DayMomentum'][-1]/2 
			for i in range(0,daysIntoFuture): 	#Add new days to the end for crystal ball predictions
				momentum = (momentum + ls)/2 * (100+random.randint(-3,4))/100
				a = (self.pricePredictions['estLow'][-1] + self.pricePredictions['estHigh'][-1])/2
				if ls >= minActionableSlope and ss >= minActionableSlope: #++
					l = a * (1+momentum) + random.randint(round(-deviation*200),round(deviation*10))/100
					h = a * (1+momentum) + random.randint(round(-deviation*10),round(deviation*200))/100
				elif ls >= minActionableSlope and ss < -minActionableSlope: #+-
					l = a * (1+momentum) + random.randint(round(-deviation*200),round(deviation*10))/100
					h = a * (1+momentum) + random.randint(round(-deviation*10),round(deviation*400))/100
				elif ls < -minActionableSlope and ss >= minActionableSlope: #-+
					l = a * (1+momentum) + random.randint(round(-deviation*200),round(deviation*10))/100
					h = a * (1+momentum) + random.randint(round(-deviation*10),round(deviation*400))/100
				elif ls < -minActionableSlope and ss < -minActionableSlope: #--
					l = a * (1+momentum) + random.randint(round(-deviation*200),round(deviation*10))/100
					h = a * (1+momentum) + random.randint(round(-deviation*10),round(deviation*200))/100
				else:	#==
					l = a * (1+momentum) + random.randint(round(-deviation*200),round(deviation*10))/100
					h = a * (1+momentum) + random.randint(round(-deviation*10),round(deviation*200))/100
				self.pricePredictions.loc[d + BDay(i+1), 'estLow'] = l
				self.pricePredictions.loc[d + BDay(i+1), 'estHigh'] = h								
			self.pricePredictions['estAverage']	= (self.pricePredictions['estLow'] + self.pricePredictions['estHigh'])/2
		elif method==3:	#Use LSTM to predict prices
			from _classes.SeriesPrediction import StockPredictionNN
			temporarilyNormalize = False
			if not self.pricesNormalized:
				temporarilyNormalize = True
				self.NormalizePrices()
			model = StockPredictionNN(baseModelName='Prices', UseLSTM=True)
			FieldList = None
			#FieldList = BaseFieldList
			model.LoadSource(sourceDF=self.historicalPrices, FieldList=FieldList, window_size=1)
			model.LoadTarget(targetDF=None, prediction_target_days=daysIntoFuture)
			model.MakeBatches(batch_size=64, train_test_split=.93)
			model.BuildModel()
			if (not model.Load() and NNTrainingEpochs == 0): NNTrainingEpochs = 250
			if (NNTrainingEpochs > 0): 
				model.Train(epochs=NNTrainingEpochs)
				model.Save()
			model.Predict(True)
			self.pricePredictions = model.GetTrainingResults(False, False)
			self.pricePredictions = self.pricePredictions.rename(columns={'Average':'estAverage'})
			deviation = self.historicalPrices['15DavDeviation'][-1]/2
			self.pricePredictions['estLow'] = self.pricePredictions['estAverage'] * (1 - deviation)
			self.pricePredictions['estHigh'] = self.pricePredictions['estAverage'] * (1 + deviation)
			if temporarilyNormalize: 
				self.predictionsLoaded = True
				self.NormalizePrices()
		elif method==4:	#Use CNN to predict prices
			from _classes.SeriesPrediction import StockPredictionNN
			temporarilyNormalize = False
			if not self.pricesNormalized:
				temporarilyNormalize = True
				self.NormalizePrices()
			model = StockPredictionNN(baseModelName='Prices', UseLSTM=False)
			FieldList = BaseFieldList
			model.LoadSource(sourceDF=self.historicalPrices, FieldList=FieldList, window_size=daysIntoFuture*16)
			model.LoadTarget(targetDF=None, prediction_target_days=daysIntoFuture)
			model.MakeBatches(batch_size=64, train_test_split=.93)
			model.BuildModel()
			if (not model.Load() and NNTrainingEpochs == 0): NNTrainingEpochs = 250
			if (NNTrainingEpochs > 0): 
				model.Train(epochs=NNTrainingEpochs)
				model.Save()
			model.Predict(True)
			self.pricePredictions = model.GetTrainingResults(False, False)
			self.pricePredictions['estAverage'] = (self.pricePredictions['Low'] + self.pricePredictions['High'] + self.pricePredictions['Open'] + self.pricePredictions['Close'])/4
			deviation = self.historicalPrices['15DavDeviation'][-1]/2
			self.pricePredictions['estLow'] = self.pricePredictions['estAverage'] * (1 - deviation)
			self.pricePredictions['estHigh'] = self.pricePredictions['estAverage'] * (1 + deviation)
			self.pricePredictions = self.pricePredictions[['estLow','estAverage','estHigh']]
			if temporarilyNormalize: 
				self.predictionsLoaded = True
				self.NormalizePrices()
		self.pricePredictions.fillna(0, inplace=True)
		x = self.pricePredictions.join(self.historicalPrices)
		x['PercentageDeviation'] = abs((x['Average']-x['estAverage'])/x['Average'])
		self.predictionDeviation = x['PercentageDeviation'].tail(round(x.shape[0]/4)).mean() #Average of the last 25%, this is being kind as it includes some training data
		self.predictionsLoaded = True
		return True
	
	def PredictFuturePrice(self,fromDate:datetime, daysForward:int=1,method:int=1):
		fromDate=DateFormatDatabase(fromDate)
		low,high,price,momentum,deviation = self.historicalPrices.loc[fromDate, ['Low','High','Average', '3DayMomentum','15DavDeviation']]
		#print(p,m,s)
		if method==0:
			futureLow = low
			futureHigh = high
		else:  
			futureLow = price * (1 + daysForward * momentum) - (price * deviation/2)
			futureHigh = price * (1 + daysForward * momentum) + (price * deviation/2)
		return futureLow, futureHigh	

	def GetDateFromIndex(self,indexLocation:int):
		d = self.historicalPrices.index.values[indexLocation]
		return DateFormatDatabase(str(d)[:10])

	def GetPrice(self,forDate:datetime, verbose:bool=False):
		forDate = DateFormatDatabase(forDate)
		i = 0
		while i < 6 and not PandaIsInIndex(self.historicalPrices, forDate):
			forDate -= datetime.timedelta(days=1)
			i+=1
		try:
			r = self.historicalPrices.loc[forDate]['Average']
		except:			
			if verbose: print('Unable to get price for ' + self.stockTicker + ' on ' + str(forDate))	
			r = 0
		return r
		
	def GetPriceSnapshot(self,forDate:datetime, verbose:bool=False):
		forDate = DateFormatDatabase(forDate)
		sn = PriceSnapshot()
		sn.ticker = self.stockTicker
		i = 0
		while i < 6 and not PandaIsInIndex(self.historicalPrices, forDate):
			forDate -= datetime.timedelta(days=1)
			i+=1
		sn.snapShotDate = forDate 
		try:
			sn.high,sn.low,sn.open,sn.close,sn.oneDayAverage,sn.twoDayAverage,sn.fiveDayAverage,sn.shortEMA,sn.shortEMASlope,sn.longEMA,sn.longEMASlope,sn.channelHigh,sn.channelLow,sn.oneDayApc,sn.oneDayDeviation,sn.fiveDayDeviation,sn.fifteenDayDeviation,sn.dailyGain,sn.monthlyGain,sn.monthlyLossStd =self.historicalPrices.loc[forDate,['High','Low','Open','Close','Average','2DayAv','5DayAv','shortEMA','shortEMASlope','longEMA','longEMASlope','channelHigh', 'channelLow','1DayApc','1DayDeviation','5DavDeviation','15DavDeviation','dailyGain','monthlyGain','monthlyLossStd']]
		except:
			sn.high, sn.low, sn.open,sn.close = 0,0,0,0
			if verbose: print('Unable to get price snapshot for ' + self.stockTicker + ' on ' + str(forDate))	
		if sn.high > 0:
			if sn.longEMASlope < 0:
				if sn.shortEMASlope > 0:	#bounce or early recovery
					sn.nextDayTarget = min(sn.oneDayAverage, sn.twoDayAverage)
				else:
					sn.nextDayTarget = min(sn.low, sn.twoDayAverage)			
			else:
				if sn.shortEMASlope < 0:	#correction or early downturn
					sn.nextDayTarget = max(sn.oneDayAverage, (sn.twoDayAverage*2)-sn.oneDayAverage) + (sn.oneDayAverage * (sn.longEMASlope))
				else:
					sn.nextDayTarget = max(sn.oneDayAverage, sn.twoDayAverage) + (sn.oneDayAverage * sn.longEMASlope)

			sn.nextDayTarget = max(sn.oneDayAverage, sn.twoDayAverage) + (sn.oneDayAverage * sn.longEMASlope)
			if not self.predictionsLoaded or forDate >= self.historyEndDate:
				sn.estLow,sn.estHigh= self.PredictFuturePrice(forDate,1)
			else:
				tomorrow =  forDate.date() + datetime.timedelta(days=1) 
				sn.estLow,sn.estHigh= self.pricePredictions.loc[tomorrow,['estLow','estHigh']]
		return sn

	def GetCurrentPriceSnapshot(self): return self.GetPriceSnapshot(self.historyEndDate)

	def GetPriceHistory(self, fieldList:list = None, includePredictions:bool = False):
		if fieldList == None:
			r = self.historicalPrices.copy() #best to pass back copies instead of reference.
		else:
			r = self.historicalPrices[fieldList].copy() #best to pass back copies instead of reference.			
		if includePredictions: r = r.join(self.pricePredictions, how='outer')
		return r
		
	def GetPricePredictions(self):
		return self.pricePredictions.copy()  #best to pass back copies instead of reference.

	def GraphData(self, endDate:datetime=None, daysToGraph:int=90, graphTitle:str=None, includePredictions:bool=False, saveToFile:bool=False, fileNameSuffix:str=None, saveToFolder:str='', dpi:int=600, trimHistoricalPredictions:bool = True):
		PlotInitDefaults()
		if includePredictions:
			if not self.predictionsLoaded: self.PredictPrices()
			if endDate == None: endDate = self.pricePredictions.index.max()
			endDate = DateFormatDatabase(endDate)
			startDate = endDate - BDay(daysToGraph) 
			fieldSet = ['High','Low', 'channelHigh', 'channelLow', 'estHigh','estLow']
			if trimHistoricalPredictions: 
				y = self.pricePredictions[self.pricePredictions.index >= self.historyEndDate]
				x = self.historicalPrices.join(y, how='outer')
			else: 
				fieldSet = ['High','Low', 'estHigh','estLow']
				x = self.historicalPrices.join(self.pricePredictions, how='outer')
			if daysToGraph > 1800:	fieldSet = ['Average', 'estHigh','estLow']
		else:
			if endDate == None: endDate = self.historyEndDate
			endDate = DateFormatDatabase(endDate)
			startDate = endDate - BDay(daysToGraph) 
			fieldSet = ['High','Low', 'channelHigh', 'channelLow']
			if daysToGraph > 1800: fieldSet = ['Average']
			x = self.historicalPrices
		if fileNameSuffix == None: fileNameSuffix = str(endDate)[:10] + '_' + str(daysToGraph) + 'days'
		if graphTitle==None: graphTitle = self.stockTicker + ' ' + fileNameSuffix 
		ax=x.loc[startDate:endDate,fieldSet].plot(title=graphTitle, linewidth=.75)			
		ax.set_xlabel('Date')
		ax.set_ylabel('Price')
		ax.tick_params(axis='x', rotation=70)
		ax.grid(b=True, which='major', color='black', linestyle='solid', linewidth=.5)
		ax.grid(b=True, which='minor', color='0.65', linestyle='solid', linewidth=.3)

		PlotScalerDateAdjust(startDate, endDate, ax)
		if saveToFile:
			if not fileNameSuffix =='': fileNameSuffix = '_' + fileNameSuffix
			if saveToFolder=='': saveToFolder = self._dataFolderCharts
			if not saveToFolder.endswith('/'): saveToFolder = saveToFolder + '/'
			if CreateFolder(saveToFolder): 	plt.savefig(saveToFolder + self.stockTicker + fileNameSuffix + '.png', dpi=dpi)			
		else:
			plt.show()
		plt.close('all')

class Tranche: #interface for handling actions on a chunk of funds
	ticker = ''
	size = 0
	units = 0
	available = True
	purchased = False
	marketOrder = False
	sold = False
	expired = False
	dateBuyOrderPlaced = None
	dateBuyOrderFilled = None
	dateSellOrderPlaced = None
	dateSellOrderFilled = None
	buyOrderPrice = 0
	purchasePrice = 0
	sellOrderPrice = 0
	sellPrice = 0
	latestPrice = 0
	expireAfterDays = 0
	_verbose = False
	
	def __init__(self, size:int=1000):
		self.size = size
		
	def AdjustBuyUnits(self, newValue:int):	
		if self._verbose: print(' Adjusting Buy from ' + str(self.units) + ' to ' + str(newValue) + ' units (' + self.ticker + ')')
		self.units=newValue

	def CancelOrder(self, verbose:bool=False): 
		self.marketOrder=False
		self.expireAfterDays=0
		if self.purchased:
			if verbose: print(' Sell order on ', self.ticker, ' canceled.')
			self.dateSellOrderPlaced = None
			self.sellOrderPrice = 0
			self.expired=False
		else:
			if verbose: print(' Buy order for ', self.ticker, ' canceled.')
			self.Recycle()
		
	def Expire(self):
		if not self.purchased:  #cancel buy
			if self._verbose: print(' Buy order from ' + str(self.dateBuyOrderPlaced) + ' has expired (' + self.ticker + ')')
			self.Recycle()
		else: #cancel sell
			if self._verbose: print(' Sell order from ' + str(self.dateSellOrderPlaced) + ' has expired (' + self.ticker + ')')
			self.dateSellOrderPlaced=None
			self.sellOrderPrice=0
			self.marketOrder = False
			self.expireAfterDays = 0
			self.expired=False

	def PlaceBuy(self, ticker:str, price:float, datePlaced:datetime, marketOrder:bool=False, expireAfterDays:int=10, verbose:bool=False):
		#returns amount taken out of circulation by the order
		r = 0
		self._verbose = verbose
		if self.available and price > 0:
			self.available = False
			self.ticker=ticker
			self.marketOrder = marketOrder
			self.dateBuyOrderPlaced = datePlaced
			self.buyOrderPrice=price
			self.units = round(self.size/price)
			self.purchased = False
			self.expireAfterDays=expireAfterDays
			r=(price*self.units)
			if self._verbose: 
				if marketOrder:
					print(datePlaced, ' Buy placed at Market (' + str(price) + ') for ' + str(self.units) + ' Cost ' + str(r) + '(' + self.ticker + ')')
				else:
					print(datePlaced, ' Buy placed at ' + str(price) + ' for ' + str(self.units) + ' Cost ' + str(r) + '(' + self.ticker + ')')
		return r
		
	def PlaceSell(self, price, datePlaced, marketOrder:bool=False, expireAfterDays:int=10, verbose:bool=False):
		r = False
		self._verbose = verbose
		if self.purchased and price > 0:
			self.sold = False
			self.dateSellOrderPlaced = datePlaced
			self.sellOrderPrice = price
			self.marketOrder = marketOrder
			self.expireAfterDays=expireAfterDays
			if self._verbose: 
				if marketOrder: 
					print(datePlaced, ' Sell placed at Market for ' + str(self.units) + ' (' + self.ticker + ')')
				else:
					print(datePlaced, ' Sell placed at ' + str(price) + ' for ' + str(self.units) + ' (' + self.ticker + ')')
			r=True
		return r

	def PrintDetails(self):
		if not self.ticker =='' or True:
			print("Stock: " + self.ticker)
			print("units: " + str(self.units))
			print("available: " + str(self.available))
			print("purchased: " + str(self.purchased))
			print("dateBuyOrderPlaced: " + str(self.dateBuyOrderPlaced))
			print("dateBuyOrderFilled: " + str(self.dateBuyOrderFilled))
			print("buyOrderPrice: " + str(self.buyOrderPrice))
			print("purchasePrice: " + str(self.purchasePrice))
			print("dateSellOrderPlaced: " + str(self.dateSellOrderPlaced))
			print("dateSellOrderFilled: " + str(self.dateSellOrderFilled))
			print("sellOrderPrice: " + str(self.sellOrderPrice))
			print("sellPrice: " + str(self.sellPrice))
			print("latestPrice: " + str(self.latestPrice))
			print("\n")

	def Recycle(self):
		self.ticker = ""
		self.units = 0
		self.available = True
		self.purchased = False
		self.sold = False
		self.expired = False
		self.marketOrder = False
		self.dateBuyOrderPlaced = None
		self.dateBuyOrderFilled = None
		self.dateSellOrderPlaced = None
		self.dateSellOrderFilled = None
		self.latestPrice=None
		self.buyOrderPrice = 0
		self.purchasePrice = 0
		self.sellOrderPrice = 0
		self.sellPrice = 0
		self.expireAfterDays = 0
		self._verbose=False
	
	def UpdateStatus(self, price, dateChecked):
		#Returns True if the order had action: filled or expired.
		r = False
		if price > 0: 
			self.latestPrice = price
			if self.buyOrderPrice > 0 and not self.purchased:
				if self.buyOrderPrice >= price or self.marketOrder:
					self.dateBuyOrderFilled = dateChecked
					self.purchasePrice = price
					self.purchased=True
					if self._verbose: print(dateChecked, ' Buy ordered on ' + str(self.dateBuyOrderPlaced) + ' filled for ' + str(price) + ' (' + self.ticker + ')')
					r=True
				else:
					self.expired = (DateDiffDays(self.dateBuyOrderPlaced , dateChecked) > self.expireAfterDays)
					if self.expired and self._verbose: print(dateChecked, 'Buy order from ' + str(self.dateBuyOrderPlaced) + ' expired.')
					r = self.expired
			elif self.sellOrderPrice > 0 and not self.sold:
				if self.sellOrderPrice <= price or self.marketOrder:
					self.dateSellOrderFilled = dateChecked
					self.sellPrice = price
					self.sold=True
					self.expired=False
					if self._verbose: print(dateChecked, ' Sell ordered on ' + str(self.dateSellOrderPlaced) + ' filled for ' + str(price) + ' (' + self.ticker + ')') 
					r=True
				else:
					self.expired = (DateDiffDays(self.dateSellOrderPlaced, dateChecked) > self.expireAfterDays)
					if self.expired and self._verbose: print(dateChecked, 'Sell order from ' + str(self.dateSellOrderPlaced) + ' expired.')
					r = self.expired
			else:
				r=False
		return r

class Position:	#Simple interface for open positions
	def __init__(self, t:Tranche):
		self._t = t
		self.ticker = t.ticker
	def CancelSell(self): 
		if self._t.purchased: self._t.CancelOrder(verbose=True)
	def CurrentValue(self): return self._t.units * self._t.latestPrice
	def Sell(self, price:float, datePlaced:datetime, marketOrder:bool=False, expireAfterDays:int=90): self._t.PlaceSell(price=price, datePlaced=datePlaced, marketOrder=marketOrder, expireAfterDays=expireAfterDays, verbose=True)
	def SellPending(self): return (self._t.sellOrderPrice >0) and not (self._t.sold or  self._t.expired)
	def LatestPrice(self): return self._t.latestPrice
	
class Portfolio:
	portfolioName = ''
	tradeHistory = [] #DataFrame of trades.  Note: though you can trade more than once a day it is only going to keep one entry per day per stock
	dailyValue = []	  #DataFrame for the value at the end of each day
	_cash=0
	_fundsCommittedToOrders=0
	_commisionCost = 0
	_tranches = []			#Sets of funds for investing, rather than just a pool of cash.  Simplifies accounting.
	_tranchCount = 0
	_verbose = False
	
	def __del__(self):
		self._cash = 0
		self._tranches = None

	def __init__(self, portfolioName:str, startDate:datetime, totalFunds:int=10000, tranchSize:int=1000, trackHistory:bool=True, verbose:bool=True):
		self.portfolioName = portfolioName
		self._cash = totalFunds
		self._fundsCommittedToOrders = 0
		self._verbose = verbose
		self._tranchCount = round(totalFunds/tranchSize)
		self._tranches = [Tranche(tranchSize) for x in range(self._tranchCount)]
		self.dailyValue = pd.DataFrame([[startDate,totalFunds,0,totalFunds]], columns=list(['Date','CashValue','AssetValue','TotalValue']))
		self.dailyValue.set_index(['Date'], inplace=True)
		self.trackHistory = trackHistory
		if trackHistory: 
			self.tradeHistory = pd.DataFrame(columns=['dateBuyOrderPlaced','ticker','dateBuyOrderFilled','dateSellOrderPlaced','dateSellOrderFilled','units','buyOrderPrice','purchasePrice','sellOrderPrice','sellPrice','NetChange'])
			self.tradeHistory.set_index(['dateBuyOrderPlaced','ticker'], inplace=True)

	#----------------------  Status and position info  ---------------------------------------
	def AccountingError(self):
		r = False
		if not self.ValidateFundsCommittedToOrders() == 0: 
			print(' Accounting error: inaccurcy in funds committed to orders!')
			r=True
		if self.FundsAvailable() + self._tranchCount*self._commisionCost < 0: 
			print(' Accounting error: negative cash balance.  (Cash, CommittedFunds, AvailableFunds) ', self._cash, self._fundsCommittedToOrders, self.FundsAvailable())
			r=True
		return r

	def FundsAvailable(self): return (self._cash - self._fundsCommittedToOrders)
	
	def PendingOrders(self):
		a, b, s, l = self.PositionSummary()
		return (b+s > 0)

	def GetPositions(self, ticker:str='', asDataFrame:bool=False):	#returns reference to the tranche of active positions or a dataframe with counts
		r = []
		for t in self._tranches:
			if t.purchased and (t.ticker==ticker or ticker==''): 
				p = Position(t)
				r.append(p)
		if asDataFrame:
			y=[]
			for x in r: y.append(x.ticker)
			r = pd.DataFrame(y,columns=list(['Ticker']))
			r = r.groupby(['Ticker']).size().reset_index(name='CurrentHoldings')
			r.set_index(['Ticker'], inplace=True)
		return r

	def PositionSummary(self):
		available=0
		buyPending=0
		sellPending=0
		longPostition = 0
		for t in self._tranches:
			if t.available:
				available +=1
			elif  not t.purchased:
				buyPending +=1
			elif t.purchased and t.dateSellOrderPlaced==None:
				longPostition +=1
			elif t.dateBuyOrderPlaced:
				sellPending +=1
		return available, buyPending, sellPending, longPostition			

	def PrintPositions(self):
		i=0
		for t in self._tranches:
			if not t.ticker =='' or True:
				print('Set: ' + str(i))
				t.PrintDetails()
			i=i+1
		print('Funds committed to orders: ' + str(self._fundsCommittedToOrders))
		print('available funds: ' + str(self._cash - self._fundsCommittedToOrders))

	def TranchesAvailable(self):
		a, b, s, l = self.PositionSummary()
		return a

	def ValidateFundsCommittedToOrders(self, FixIt:bool=False):
		#Returns difference between recorded value and actual
		x=0
		for t in self._tranches:
			if not t.available and not t.purchased: 
				x = x + (t.units*t.buyOrderPrice) + self._commisionCost
		if round(self._fundsCommittedToOrders, 5) == round(x,5): self._fundsCommittedToOrders=x
		if not (self._fundsCommittedToOrders - x) ==0:
			if FixIt: 
				self._fundsCommittedToOrders = x
			else:
				print( 'Committed funds variance actual/recorded', x, self._fundsCommittedToOrders)
		return (self._fundsCommittedToOrders - x)

	def Value(self):
		assetValue=0
		for t in self._tranches:
			if t.purchased:
				assetValue = assetValue + (t.units*t.latestPrice)
		return self._cash, assetValue
		
	def ReEvaluateTrancheCount(self, verbose:bool=False):
		#Portfolio performance may require adjusting the available Tranches
		tranchSize = self._tranches[0].size
		c = self._tranchCount
		availableTranches,_,_,_ = self.PositionSummary()
		availableFunds = self._cash - self._fundsCommittedToOrders
		targetAvailable = int(availableFunds/tranchSize)
		if targetAvailable > availableTranches:
			if verbose: 
				print(' Available Funds: ', availableFunds, availableTranches * tranchSize)
				print(' Adding ' + str(targetAvailable - availableTranches) + ' new Tranches to portfolio..')
			for i in range(targetAvailable - availableTranches):
				self._tranches.append(Tranche(tranchSize))
				self._tranchCount +=1
		elif targetAvailable < availableTranches:
			if verbose: print( 'Removing ' + str(availableTranches - targetAvailable) + ' tranches from portfolio..')
			#print(targetAvailable, availableFunds, tranchSize, availableTranches)
			i = self._tranchCount-1
			while i > 0:
				if self._tranches[i].available and targetAvailable < availableTranches:
					if verbose: 
						print(' Available Funds: ', availableFunds, availableTranches * tranchSize)
						print(' Removing tranch at ', i)
					self._tranches.pop(i)	#remove last available
					self._tranchCount -=1
					availableTranches -=1
				i -=1


	#--------------------------------------  Order interface  ---------------------------------------
	def CancelAllOrders(self, currentDate:datetime):
		for t in self._tranches:
			t.CancelOrder()
		#for t in self._tranches:						self.CheckOrders(t.ticker, t.latestPrice, currentDate) 

	def PlaceBuy(self, ticker:str, price:float, datePlaced:datetime, marketOrder:bool=False, expireAfterDays:int=10, verbose:bool=False):
		#Place with first available tranch, returns True if order was placed
		r=False
		price = round(price, 3)
		oldestExistingOrder = None
		availableCash = self.FundsAvailable()
		units=0
		if price > 0: units = int(self._tranches[0].size/price)
		cost = units*price + self._commisionCost
		if availableCash < cost and units > 2:
			units -=1
			cost = units*price + self._commisionCost
		if units == 0 or availableCash < cost:
			if verbose: 
				if price==0:
					print( 'Unable to purchase ' + ticker + '.  Price lookup failed.')
				else:
					print( 'Unable to purchase ' + ticker + '.  Price (' + str(price) + ') exceeds available funds', availableCash)
		else:	
			for t in self._tranches: #Find available 
				if t.available :	#Place new order
					self._fundsCommittedToOrders = self._fundsCommittedToOrders + cost 
					x = self._commisionCost + t.PlaceBuy(ticker=ticker, price=price, datePlaced=datePlaced, marketOrder=marketOrder, expireAfterDays=expireAfterDays, verbose=verbose) 
					if not x == cost: #insufficient funds for full purchase
						if verbose: print(' Expected cost changed from', cost, 'to', x)
						self._fundsCommittedToOrders = self._fundsCommittedToOrders - cost + x + self._commisionCost
					r=True
					break
				elif not t.purchased and t.ticker == ticker:	#Might have to replace existing order
					if oldestExistingOrder == None:
						oldestExistingOrder=t.dateBuyOrderPlaced
					else:
						if oldestExistingOrder > t.dateBuyOrderPlaced: oldestExistingOrder=t.dateBuyOrderPlaced
		if not r and units > 0 and False:	#We could allow replacing oldest existing order
			if oldestExistingOrder == None:
				if self.TranchesAvailable() > 0:
					if verbose: print(' Unable to buy ' + str(units) + ' of ' + ticker + ' with funds available: ' + str(FundsAvailable))
				else: 
					if verbose: print(' Unable to buy ' + ticker + ' no tranches available')
			else:
				for t in self._tranches:
					if not t.purchased and t.ticker == ticker and oldestExistingOrder==t.dateBuyOrderPlaced:
						if verbose: print(' No tranch available... replacing order from ' + str(oldestExistingOrder))
						oldCost = t.buyOrderPrice * t.units + self._commisionCost
						if verbose: print(' Replacing Buy order for ' + ticker + ' from ' + str(t.buyOrderPrice) + ' to ' + str(price))
						t.units = units
						t.buyOrderPrice = price
						t.dateBuyOrderPlaced = datePlaced
						t.marketOrder = marketOrder
						self._fundsCommittedToOrders = self._fundsCommittedToOrders - oldCost + cost 
						r=True
						break		
		return r

	def PlaceSell(self, ticker:str, price:float, datePlaced:datetime, marketOrder:bool=False, expireAfterDays:int=10, datepurchased:datetime=None, verbose:bool=False):
		#Returns True if order was placed
		r=False
		price = round(price, 3)
		for t in self._tranches:
			if t.ticker == ticker and t.purchased and t.sellOrderPrice==0 and (datepurchased is None or t.dateBuyOrderFilled == datepurchased):
				t.PlaceSell(price=price, datePlaced=datePlaced, marketOrder=marketOrder, expireAfterDays=expireAfterDays, verbose=verbose)
				r=True
				break
		if not r:	#couldn't find one without a sell, try to update an existing sell order
			for t in self._tranches:
				if t.ticker == ticker and t.purchased:
					if verbose: print(' Updating existing sell order ')
					t.PlaceSell(price=price, datePlaced=datePlaced, marketOrder=marketOrder, expireAfterDays=expireAfterDays, verbose=verbose)
					r=True
					break					
		return r

	def SellAllPositions(self, datePlaced:datetime, ticker:str='', verbose:bool=False):
		for t in self._tranches:
			if t.purchased and (t.ticker==ticker or ticker==''): 
				t.PlaceSell(price=t.latestPrice, datePlaced=datePlaced, marketOrder=True, expireAfterDays=5, verbose=verbose)
		self.ProcessDay(withIncrement=False)

	#--------------------------------------  Order Processing ---------------------------------------
	def _CheckOrders(self, ticker, price, dateChecked):
		#check if there was action on any pending orders and update current price of tranche
		price = round(price, 3)
		for t in self._tranches:
			if t.ticker == ticker:
				r = t.UpdateStatus(price, dateChecked)
				if r:	#Order was filled, update account
					if t.expired:
						if not t.purchased: self._fundsCommittedToOrders = self._fundsCommittedToOrders - (t.units*t.buyOrderPrice) - self._commisionCost	#return ear marked order fund
						t.Expire()
					elif t.sold:
						self._cash = self._cash + (t.units*t.sellPrice) - self._commisionCost
						if self._verbose: print(' Commission charged for Sell: ' + str(self._commisionCost))
						if self.trackHistory:
							self.tradeHistory.loc[(t.dateBuyOrderPlaced, t.ticker)]=[t.dateBuyOrderFilled,t.dateSellOrderPlaced,t.dateSellOrderFilled,t.units,t.buyOrderPrice,t.purchasePrice,t.sellOrderPrice,t.sellPrice,((t.sellPrice - t.purchasePrice)*t.units)-self._commisionCost*2] 
						t.Recycle()
					elif t.purchased:
						self._fundsCommittedToOrders = self._fundsCommittedToOrders - (t.units*t.buyOrderPrice) - self._commisionCost	#return ear marked order fund
						fundsavailable = self._cash - abs(self._fundsCommittedToOrders)
						if t.marketOrder:
							actualCost = t.units*price
							if (fundsavailable - actualCost - self._commisionCost) < 25:	#insufficient funds
								unitsCanAfford = max(round((fundsavailable - self._commisionCost)/price)-1, 0)
								if self._verbose:
									print(' Ajusting units on market order for ' + ticker + ' Price: ', price, ' Requested Units: ', t.units,  ' Can afford:', unitsCanAfford)
									print(' Cash: ', self._cash, ' Committed Funds: ', self._fundsCommittedToOrders, ' Available: ', fundsavailable)
								if unitsCanAfford ==0:
									t.Recycle()
								else:
									t.AdjustBuyUnits(unitsCanAfford)
						if t.units == 0:
							if self._verbose: print( 'Can not afford any ' + ticker + ' at market ' + str(price) + ' canceling Buy')
							t.Recycle()
						else:
							self._cash = self._cash - (t.units*price) - self._commisionCost 
							if self._verbose: print(' Commission charged for Buy: ' + str(self._commisionCost))		
							if self.trackHistory:							
								self.tradeHistory.loc[(t.dateBuyOrderPlaced,t.ticker), 'dateBuyOrderFilled']=t.dateBuyOrderFilled #Create the row
								self.tradeHistory.loc[(t.dateBuyOrderPlaced,t.ticker)]=[t.dateBuyOrderFilled,t.dateSellOrderPlaced,t.dateSellOrderFilled,t.units,t.buyOrderPrice,t.purchasePrice,t.sellOrderPrice,t.sellPrice,''] 
						
	def _CheckPriceSequence(self, ticker, p1, p2, dateChecked):
		#approximate a price sequence between given prices
		steps=40
		if p1==p2:
			self._CheckOrders(ticker, p1, dateChecked)		
		else:
			step = (p2-p1)/steps
			for i in range(steps):
				p = round(p1 + i * step, 3)
				self._CheckOrders(ticker, p, dateChecked)
			self._CheckOrders(ticker, p2, dateChecked)	

	def ProcessDaysOrders(self, ticker, open, high, low, close, dateChecked):
		#approximate a sequence of the days prices for given ticker and check orders for each
		if self.PendingOrders() > 0:
			p2=low
			p3=high
			if (high - open) < (open - low):
				p2=high
				p3=low
			#print(' Given price sequence      ' + str(open) + ' ' + str(high) + ' ' + str(low) + ' ' + str(close))
			#print(' Estimating price sequence ' + str(open) + ' ' + str(p2) + ' ' + str(p3) + ' ' + str(close))
			self._CheckPriceSequence(ticker, open, p2, dateChecked)
			self._CheckPriceSequence(ticker, p2, p3, dateChecked)
			self._CheckPriceSequence(ticker, p3, close, dateChecked)
		else:
			self._CheckOrders(ticker, close, dateChecked)	#No open orders but still need to update last prices
		self.ValidateFundsCommittedToOrders(True)
		_cashValue, assetValue = self.Value()
		self.dailyValue.loc[dateChecked]=[_cashValue,assetValue,_cashValue + assetValue] 

	#--------------------------------------  Closing Reporting ---------------------------------------
	def SaveTradeHistoryToFile(self, foldername:str, addTimeStamp:bool = False):
		if CreateFolder(foldername):
			filePath = foldername + self.portfolioName 
			if addTimeStamp: filePath += '_' + GetDateTimeStamp()
			filePath += '_trades.csv'
			if self.trackHistory:
				self.tradeHistory.to_csv(filePath)

	def SaveDailyValueToFile(self, foldername:str, addTimeStamp:bool = False):
		if CreateFolder(foldername):
			filePath = foldername + self.portfolioName 
			if addTimeStamp: filePath += '_' + GetDateTimeStamp()
			filePath+= '_dailyvalue.csv'
			self.dailyValue.to_csv(filePath)
		
class TradingModel(Portfolio):
	#Extends Portfolio to trading environment for testing models
	modelName = None
	modelStartDate  = None	
	modelEndDate = None
	modelReady = False
	currentDate = None
	priceHistory = []  #list of price histories for each stock in _stockTickerList
	startingValue = 0 
	verbose = False
	_stockTickerList = []	#list of stocks currently held
	_dataFolderTradeModel = 'data/trademodel/'
	Custom1 = None	#can be used to store custom values when using the model
	Custom2 = None
	_NormalizePrices = False

	def __init__(self, modelName:str, startingTicker:str, startDate:datetime, durationInYears:int, totalFunds:int, tranchSize:int=1000,verbose:bool=False, trackHistory:bool=True):
		#pricesAsPercentages:bool=False would be good but often results in Nan values
		#expects date format in local format, from there everything will be converted to database format				
		startDate = DateFormatDatabase(startDate)
		endDate = startDate + datetime.timedelta(days=365 * durationInYears)
		self.modelReady = False
		CreateFolder(self._dataFolderTradeModel)
		p = PricingData(startingTicker)
		if p.LoadHistory(requestedStartDate=startDate, requestedEndDate=endDate, verbose=verbose): 
			if verbose: print(' Loading ' + startingTicker)
			p.CalculateStats()
			p.TrimToDateRange(startDate - datetime.timedelta(days=60), endDate + datetime.timedelta(days=10))
			self.priceHistory = [p]
			if not PandaIsInIndex(p.historicalPrices, startDate): startDate += datetime.timedelta(days=1)
			if not PandaIsInIndex(p.historicalPrices, startDate): startDate += datetime.timedelta(days=1)
			if not PandaIsInIndex(p.historicalPrices, startDate): startDate += datetime.timedelta(days=1)
			if not PandaIsInIndex(p.historicalPrices, endDate): endDate -= datetime.timedelta(days=1)
			if not PandaIsInIndex(p.historicalPrices, endDate): endDate -= datetime.timedelta(days=1)
			if not PandaIsInIndex(p.historicalPrices, endDate): endDate -= datetime.timedelta(days=1)
			self.modelStartDate = startDate
			self.modelEndDate = endDate
			self.currentDate = self.modelStartDate
			modelName += '_' + str(startDate)[:10] + '_' + str(durationInYears) + 'year'
			self.modelName = modelName
			super
			self._stockTickerList = [startingTicker]
			self.startingValue = totalFunds
			self.modelReady = not(pd.isnull(self.modelStartDate))
		super(TradingModel, self).__init__(portfolioName=modelName, startDate=startDate, totalFunds=totalFunds, tranchSize=tranchSize, trackHistory=trackHistory, verbose=verbose)
		
	def __del__(self):
		self._stockTickerList = None
		del self.priceHistory[:] 
		self.priceHistory = None
		self.modelStartDate  = None	
		self.modelEndDate = None
		self.modelReady = False

	def AddStockTicker(self, ticker:str):
		r = False
		if not ticker in self._stockTickerList:
			p = PricingData(ticker)
			if self.verbose: print(' Loading price history for ' + ticker)
			if p.LoadHistory(requestedStartDate=self.modelStartDate, requestedEndDate=self.modelEndDate): 
				p.CalculateStats()
				p.TrimToDateRange(self.modelStartDate - datetime.timedelta(days=60), self.modelEndDate + datetime.timedelta(days=10))
				if len(p.historicalPrices) > len(self.priceHistory[0].historicalPrices): #first element is used for trading day indexing, replace if this is a better match
					self.priceHistory.insert(0, p)
					self._stockTickerList.insert(0, ticker)
				else:
					self.priceHistory.append(p)
					self._stockTickerList.append(ticker)
				r = True
			else:
				print( 'Unable to download price history for ticker ' + ticker)
		return r

	def CancelAllOrders(self): super(TradingModel, self).CancelAllOrders(self.currentDate)
	
	def CloseModel(self, plotResults:bool=True, saveHistoryToFile:bool=True, folderName:str='data/trademodel/', dpi:int=600):	
		cashValue, assetValue = self.Value()
		if assetValue > 0:
			self.SellAllPositions(self.currentDate)
			#self.ProcessDay()
		cashValue, assetValue = self.Value()
		netChange = cashValue + assetValue - self.startingValue 		
		if saveHistoryToFile:
			self.SaveDailyValueToFile(folderName)
			self.SaveTradeHistoryToFile(folderName)
		print('Model ' + self.modelName + ' from ' + str(self.modelStartDate)[:10] + ' to ' + str(self.modelEndDate)[:10])
		print('Cash: ' + str(round(cashValue)) + ' asset: ' + str(round(assetValue)) + ' total: ' + str(round(cashValue + assetValue)))
		print('Net change: ' + str(round(netChange)), str(round((netChange/self.startingValue) * 100, 2)) + '%')
		print('')
		if plotResults and self.trackHistory: 
			self.PlotTradeHistoryAgainstHistoricalPrices(self.tradeHistory, self.priceHistory[0].GetPriceHistory(), self.modelName)
		return cashValue + assetValue
		
	def CalculateGain(self, startDate:datetime, endDate:datetime):
		try:
			startValue = self.dailyValue['TotalValue'].at[startDate]
			endValue = self.dailyValue['TotalValue'].at[endDate]
			gain = endValue = startValue
			percentageGain = endValue/startValue
		except:
			gain = -1
			percentageGain = -1
			print('Unable to calculate gain for ', startDate, endDate)
		return gain, percentageGain
			
	def GetCustomValues(self): return self.Custom1, self.Custom2
	def GetDailyValue(self): return self.dailyValue #returns dataframe with daily value of portfolio
	def GetValueAt(self, date): 
		try:
			r = self.dailyValue['TotalValue'].at[date]
		except:
			print('Unable to return value at ', date)
			r=-1
		return r

	def GetPrice(self, ticker:str=''): 
		#returns snapshot object of yesterday's pricing info to help make decisions today
		forDate = self.currentDate + datetime.timedelta(days=-1)
		r = None
		if ticker =='':
			r = self.priceHistory[0].GetPrice(forDate)
		else:
			if not ticker in self._stockTickerList:	self.AddStockTicker(ticker)
			if ticker in self._stockTickerList:
				for ph in self.priceHistory:
					if ph.stockTicker == ticker: r = ph.GetPrice(forDate) 
		return r

	def GetPriceSnapshot(self, ticker:str=''): 
		#returns snapshot object of yesterday's pricing info to help make decisions today
		forDate = self.currentDate + datetime.timedelta(days=-1)
		r = None
		if ticker =='':
			r = self.priceHistory[0].GetPriceSnapshot(forDate)
		else:
			if not ticker in self._stockTickerList:	self.AddStockTicker(ticker)
			if ticker in self._stockTickerList:
				for ph in self.priceHistory:
					if ph.stockTicker == ticker: r = ph.GetPriceSnapshot(forDate) 
		return r

	def ModelCompleted(self):
		if self.currentDate ==None or self.modelEndDate == None: 
			r = True
			print('Model start or end date is None', self.currentDate, self.modelEndDate)
		else:
			r = self.currentDate >= self.modelEndDate
		return(r)

	def NormalizePrices(self):
		self._NormalizePrices =  not self._NormalizePrices
		for p in self.priceHistory:
			if not p.pricesNormalized: p.NormalizePrices()
		
	def PlaceBuy(self, ticker:str, price:float, marketOrder:bool=False, expireAfterDays:bool=10, verbose:bool=False):
		if not ticker in self._stockTickerList: self.AddStockTicker(ticker)	
		if ticker in self._stockTickerList:	
			if marketOrder: price = self.GetPrice(ticker)
			super(TradingModel, self).PlaceBuy(ticker, price, self.currentDate, marketOrder, expireAfterDays, verbose)
		else:
			print(' Unable to add ticker ' + ticker + ' to portfolio.')

	def PlaceSell(self, ticker:str, price:float, marketOrder:bool=False, expireAfterDays:bool=10, datepurchased:datetime=None, verbose:bool=False): 
		super(TradingModel, self).PlaceSell(ticker=ticker, price=price, datePlaced=self.currentDate, marketOrder=marketOrder, expireAfterDays=expireAfterDays, verbose=verbose)

	def PlotTradeHistoryAgainstHistoricalPrices(self, tradeHist:pd.DataFrame, priceHist:pd.DataFrame, modelName:str):
		buys = tradeHist.loc[:,['dateBuyOrderFilled','purchasePrice']]
		buys = buys.rename(columns={'dateBuyOrderFilled':'Date'})
		buys.set_index(['Date'], inplace=True)
		sells  = tradeHist.loc[:,['dateSellOrderFilled','sellPrice']]
		sells = sells.rename(columns={'dateSellOrderFilled':'Date'})
		sells.set_index(['Date'], inplace=True)
		dfTemp = priceHist.loc[:,['High','Low', 'channelHigh', 'channelLow']]
		dfTemp = dfTemp.join(buys)
		dfTemp = dfTemp.join(sells)
		PlotDataFrame(dfTemp, modelName, 'Date', 'Value')

	def ProcessDay(self, withIncrement:bool=True):
		#Process current day and increment the current date
		#if self.currentDate <= self.modelEndDate: 
		if self.verbose: 
			c, a = self.Value()
			if self.verbose: print(str(self.currentDate) + ' model: ' + self.modelName + ' _cash: ' + str(c) + ' Assets: ' + str(a))
		for ph in self.priceHistory:
			p = ph.GetPriceSnapshot(self.currentDate)
			self.ProcessDaysOrders(ph.stockTicker, p.open, p.high, p.low, p.close, self.currentDate)
			self.ReEvaluateTrancheCount()
		if withIncrement and self.currentDate <= self.modelEndDate: #increment the date
			try:
				loc = self.priceHistory[0].historicalPrices.index.get_loc(self.currentDate) + 1
				if loc < self.priceHistory[0].historicalPrices.shape[0]:
					nextDay = self.priceHistory[0].historicalPrices.index.values[loc]
					self.currentDate = DateFormatDatabase(str(nextDay)[:10])
				else:
					print('The end: ' + str(self.modelEndDate))
					self.currentDate=self.modelEndDate		
			except:
				#print(self.priceHistory[0].historicalPrices)
				print('Unable to find next date in index from ', self.currentDate)
				self.currentDate += datetime.timedelta(days=1)
	
	def SetCustomValues(self, v1, v2):
		self.Custom1 = v1
		self.custom2 = v2
		
class ForcastModel():	#used to forecast the effect of a series of trade actions, one per day, and return the net change in value.  This will mirror the given model.  Can also be used to test alternate past actions 
	def __init__(self, mirroredModel:TradingModel, daysToForecast:int = 10):
		modelName = 'Forcaster for ' + mirroredModel.modelName
		self.daysToForecast = daysToForecast
		self.startDate = mirroredModel.modelStartDate 
		durationInYears = (mirroredModel.modelEndDate-mirroredModel.modelStartDate).days/365
		self.tm = TradingModel(modelName=modelName, startingTicker=mirroredModel._stockTickerList[0], startDate=mirroredModel.modelStartDate, durationInYears=durationInYears, totalFunds=mirroredModel.startingValue, verbose=False, trackHistory=False)
		self.savedModel = TradingModel(modelName=modelName, startingTicker=mirroredModel._stockTickerList[0], startDate=mirroredModel.modelStartDate, durationInYears=durationInYears, totalFunds=mirroredModel.startingValue, verbose=False, trackHistory=False)
		self.mirroredModel = mirroredModel
		self.tm._stockTickerList = mirroredModel._stockTickerList
		self.tm.priceHistory = mirroredModel.priceHistory
		self.savedModel._stockTickerList = mirroredModel._stockTickerList
		self.savedModel.priceHistory = mirroredModel.priceHistory

	def Reset(self, updateSavedModel:bool=True):
		if updateSavedModel:
			c, a = self.mirroredModel.Value()
			self.savedModel.currentDate = self.mirroredModel.currentDate
			self.savedModel._cash=self.mirroredModel._cash
			self.savedModel._fundsCommittedToOrders=self.mirroredModel._fundsCommittedToOrders
			self.savedModel.dailyValue = pd.DataFrame([[self.mirroredModel.currentDate,c,a,c+a]], columns=list(['Date','CashValue','AssetValue','TotalValue']))
			self.savedModel.dailyValue.set_index(['Date'], inplace=True)
			for i in range(len(self.savedModel._tranches)):
				self.savedModel._tranches[i].ticker = self.mirroredModel._tranches[i].ticker
				self.savedModel._tranches[i].available = self.mirroredModel._tranches[i].available
				self.savedModel._tranches[i].size = self.mirroredModel._tranches[i].size
				self.savedModel._tranches[i].units = self.mirroredModel._tranches[i].units
				self.savedModel._tranches[i].purchased = self.mirroredModel._tranches[i].purchased
				self.savedModel._tranches[i].marketOrder = self.mirroredModel._tranches[i].marketOrder
				self.savedModel._tranches[i].sold = self.mirroredModel._tranches[i].sold
				self.savedModel._tranches[i].dateBuyOrderPlaced = self.mirroredModel._tranches[i].dateBuyOrderPlaced
				self.savedModel._tranches[i].dateBuyOrderFilled = self.mirroredModel._tranches[i].dateBuyOrderFilled
				self.savedModel._tranches[i].dateSellOrderPlaced = self.mirroredModel._tranches[i].dateSellOrderPlaced
				self.savedModel._tranches[i].dateSellOrderFilled = self.mirroredModel._tranches[i].dateSellOrderFilled
				self.savedModel._tranches[i].buyOrderPrice = self.mirroredModel._tranches[i].buyOrderPrice
				self.savedModel._tranches[i].purchasePrice = self.mirroredModel._tranches[i].purchasePrice
				self.savedModel._tranches[i].sellOrderPrice = self.mirroredModel._tranches[i].sellOrderPrice
				self.savedModel._tranches[i].sellPrice = self.mirroredModel._tranches[i].sellPrice
				self.savedModel._tranches[i].latestPrice = self.mirroredModel._tranches[i].latestPrice
				self.savedModel._tranches[i].expireAfterDays = self.mirroredModel._tranches[i].expireAfterDays
		c, a = self.savedModel.Value()
		self.startingValue = c + a
		self.tm.currentDate = self.savedModel.currentDate
		self.tm._cash=self.savedModel._cash
		self.tm._fundsCommittedToOrders=self.savedModel._fundsCommittedToOrders
		self.tm.dailyValue = pd.DataFrame([[self.savedModel.currentDate,c,a,c+a]], columns=list(['Date','CashValue','AssetValue','TotalValue']))
		self.tm.dailyValue.set_index(['Date'], inplace=True)
		for i in range(len(self.tm._tranches)):
			self.tm._tranches[i].ticker = self.savedModel._tranches[i].ticker
			self.tm._tranches[i].available = self.savedModel._tranches[i].available
			self.tm._tranches[i].size = self.savedModel._tranches[i].size
			self.tm._tranches[i].units = self.savedModel._tranches[i].units
			self.tm._tranches[i].purchased = self.savedModel._tranches[i].purchased
			self.tm._tranches[i].marketOrder = self.savedModel._tranches[i].marketOrder
			self.tm._tranches[i].sold = self.savedModel._tranches[i].sold
			self.tm._tranches[i].dateBuyOrderPlaced = self.savedModel._tranches[i].dateBuyOrderPlaced
			self.tm._tranches[i].dateBuyOrderFilled = self.savedModel._tranches[i].dateBuyOrderFilled
			self.tm._tranches[i].dateSellOrderPlaced = self.savedModel._tranches[i].dateSellOrderPlaced
			self.tm._tranches[i].dateSellOrderFilled = self.savedModel._tranches[i].dateSellOrderFilled
			self.tm._tranches[i].buyOrderPrice = self.savedModel._tranches[i].buyOrderPrice
			self.tm._tranches[i].purchasePrice = self.savedModel._tranches[i].purchasePrice
			self.tm._tranches[i].sellOrderPrice = self.savedModel._tranches[i].sellOrderPrice
			self.tm._tranches[i].sellPrice = self.savedModel._tranches[i].sellPrice
			self.tm._tranches[i].latestPrice = self.savedModel._tranches[i].latestPrice
			self.tm._tranches[i].expireAfterDays = self.savedModel._tranches[i].expireAfterDays		
		c, a = self.tm.Value()
		if self.startingValue != c + a:
			print( 'Forcast model accounting error.  ', self.startingValue, self.mirroredModel.Value(), self.savedModel.Value(), self.tm.Value())
			assert(False)
			
	def GetResult(self):
		dayCounter = len(self.tm.dailyValue)
		while dayCounter <= self.daysToForecast:  
			self.tm.ProcessDay()
			dayCounter +=1
		c, a = self.tm.Value()
		endingValue = c + a
		return endingValue - self.startingValue
		
class StockPicker():
	def __init__(self, startDate:datetime=None, endDate:datetime=None): 
		self.priceData = []
		self._stockTickerList = []
		self._startDate = startDate
		self._endDate = endDate
		
	def __del__(self): 
		self.priceData = None
		self._stockTickerList = None
		
	def AddTicker(self, ticker:str):
		if not ticker in self._stockTickerList:
			p = PricingData(ticker)
			if p.LoadHistory(self._startDate, self._endDate, verbose=True): 
				p.CalculateStats()
				self.priceData.append(p)
				self._stockTickerList.append(ticker)

	def FindOpportunities(self, currentDate:datetime, stocksToReturn:int = 5, filterOption:int = 3, minPercentGain=0.00): 
		result = []
		for i in range(len(self.priceData)):
			ticker = self.priceData[i].stockTicker
			psnap = self.priceData[i].GetPriceSnapshot(currentDate)
			if  ((psnap.shortEMA/psnap.longEMA)-1 > minPercentGain):
				if filterOption ==0: #Overbought
					if psnap.low > psnap.channelHigh: result.append(ticker)
				if filterOption ==1: #Oversold
					if psnap.high < psnap.channelLow: result.append(ticker)
				if filterOption ==1: #High price deviation
					if psnap.fiveDayDeviation > .0275: result.append(ticker)
		return result

	def GetHighestPriceMomentum(self, currentDate:datetime, longHistoryDays:int = 365, shortHistoryDays:int = 30, stocksToReturn:int = 5, filterOption:int = 3, minPercentGain=0.05, maxVolatility=.1, verbose:bool=False): 
		minDailyGain = minPercentGain/365
		candidates = pd.DataFrame(columns=list(['Ticker','hp2Year','hp1Year','hp6mo','hp3mo','hp2mo','hp1mo','currentPrice','2yearPriceChange','1yearPriceChange','6moPriceChange','2moPriceChange','1moPriceChange','dailyGain','monthlyGain','monthlyLossStd','longHistoricalValue','shortHistoricalValue','percentageChangeLongTerm','percentageChangeShortTerm']))
		candidates.set_index(['Ticker'], inplace=True)
		lookBackDateLT = currentDate + datetime.timedelta(days=-longHistoryDays)
		lookBackDateST = currentDate + datetime.timedelta(days=-shortHistoryDays)
		for i in range(len(self.priceData)):
			ticker = self.priceData[i].stockTicker
			longHistoricalValue = self.priceData[i].GetPrice(lookBackDateLT)
			shortHistoricalValue = self.priceData[i].GetPrice(lookBackDateST)				
			#hp2Year = self.priceData[i].GetPrice(currentDate )				
			#hp1Year = self.priceData[i].GetPrice(currentDate + datetime.timedelta(days=-365))				
			#hp6mo = self.priceData[i].GetPrice(currentDate + datetime.timedelta(days=-180))				
			#hp2mo = self.priceData[i].GetPrice(currentDate + datetime.timedelta(days=-60))				
			#hp1mo = self.priceData[i].GetPrice(currentDate + datetime.timedelta(days=-30))				
			#currentPrice = self.priceData[i].GetPrice(currentDate)
			s = self.priceData[i].GetPriceSnapshot(currentDate + datetime.timedelta(days=-730))
			hp2Year = s.fiveDayAverage
			s = self.priceData[i].GetPriceSnapshot(currentDate + datetime.timedelta(days=-365))
			hp1Year = s.fiveDayAverage
			s = self.priceData[i].GetPriceSnapshot(currentDate + datetime.timedelta(days=-180))
			hp6mo = s.fiveDayAverage
			s = self.priceData[i].GetPriceSnapshot(currentDate + datetime.timedelta(days=-90))
			hp3mo = s.fiveDayAverage
			s = self.priceData[i].GetPriceSnapshot(currentDate + datetime.timedelta(days=-60))
			hp2mo = s.fiveDayAverage
			s = self.priceData[i].GetPriceSnapshot(currentDate + datetime.timedelta(days=-30))
			hp1mo = s.fiveDayAverage
			s = self.priceData[i].GetPriceSnapshot(currentDate)
			#currentPrice = s.oneDayAverage	
			currentPrice = s.twoDayAverage	
			percentageChangeShortTerm = 0
			percentageChangeLongTerm = 0
			if (longHistoricalValue > 0 and currentPrice > 0 and shortHistoricalValue > 0 and hp2Year > 0 and hp1Year > 0 and hp6mo > 0 and hp2mo > 0 and hp1mo > 0): #values were loaded
				percentageChangeLongTerm = ((currentPrice/longHistoricalValue)-1)/longHistoryDays
				percentageChangeShortTerm = ((currentPrice/shortHistoricalValue)-1)/shortHistoryDays
				candidates.loc[ticker] = [hp2Year,hp1Year,hp6mo,hp3mo,hp2mo,hp1mo,currentPrice,(currentPrice/hp2Year)-1,(currentPrice/hp1Year)-1,(currentPrice/hp6mo)-1,(currentPrice/hp2mo)-1,(currentPrice/hp1mo)-1,s.dailyGain, s.monthlyGain, s.monthlyLossStd,longHistoricalValue,shortHistoricalValue,percentageChangeLongTerm, percentageChangeShortTerm]
			else:
				if currentPrice > 0 and verbose: print('Price load failed for ticker: ' + ticker, currentDate, hp2Year,hp1Year,hp6mo,hp2mo,hp1mo)
		candidates.sort_values('percentageChangeLongTerm', axis=0, ascending=False, inplace=True, kind='quicksort', na_position='last') #Most critical factor, sorting by largest long term gain
		if filterOption ==1: #high performer, recently at a discount or slowing down but not negative
			filter = (candidates['percentageChangeLongTerm'] > candidates['percentageChangeShortTerm']) & (candidates['percentageChangeLongTerm'] > minDailyGain) & (candidates['percentageChangeShortTerm'] > 0) 
			result = candidates[filter]
		elif filterOption ==2: #Long term gain meets min requirements
			filter = (candidates['percentageChangeLongTerm'] > minDailyGain) 
			result = candidates[filter]
		elif filterOption ==3: #Best overall returns 25% average yearly over 36 years which choosing top 5 sorted by best yearly average
			filter = (candidates['percentageChangeLongTerm'] > minDailyGain) & (candidates['percentageChangeShortTerm'] > 0) 
			result = candidates[filter]
		elif filterOption ==4: #Short term gain meets mine requirements
			filter =  (candidates['percentageChangeShortTerm']  > minDailyGain) 
			result = candidates[filter]
		elif filterOption ==6: #
			filter = (candidates['1yearPriceChange'] > minPercentGain) & (candidates['monthlyGain'] > 0) & (candidates['monthlyLossStd'] < maxVolatility)
			result = candidates[filter]
		elif filterOption ==7: #
			filter = (candidates['1yearPriceChange'] > minPercentGain) &  (candidates['monthlyLossStd'] < maxVolatility)
			result = candidates[filter]
		else: #no filter
			result = candidates
		result['percentageChangeLongTerm'] = result['percentageChangeLongTerm'].multiply(longHistoryDays)
		result['percentageChangeShortTerm'] = result['percentageChangeShortTerm'].multiply(shortHistoryDays)
		result = result[:stocksToReturn]
		return result