def save_symbols(symbols: SyncedList, start: dt.datetime, end: dt.datetime, listener: Listener, dir: str): """ Saves data from the range given by <start>-<end> for each symbol in <symbols>, placing output in the folder given by <dir>. For each symbol in <symbols>, fetches as much price data as possible within the given range, and saves it in a file named <symbol>.csv, in the folder given by <dir>. Args: symbols: a SyncedList of stock symbols that should be saved start: a datetime object representing the beginning of the range to try and get data from end: listener: dir: Returns: """ msg = Message() symbol = symbols.pop() while symbol is not None: msg.reset() msg.add_line("Saving " + symbol + "...") listener.send(msg) save(symbol, start, end, listener, dir) symbol = symbols.pop()
def _analyze_thread(symbols: SyncedList, tracker: IndicatorResultTracker, days: int, listener: Listener): symbol = symbols.pop() while symbol is not None: msg = Message() msg.add_line(f"Thread {threading.currentThread()} analyzing {symbol}") listener.send(msg) analyze(symbol, tracker, days, listener) symbol = symbols.pop()
def MACD_signal(symbol: str, listener: Listener, start: datetime.datetime, end: datetime.datetime, local=False, dir="") -> \ List[datetime.datetime]: """ Look for the MACD of the given stock to cross above its signal line WHILE price is also above the 200-day EMA. Return a list of all days that are the endpoints of such crosses. That is, the list of all days between <start> and <end> where MACD was above its signal line having been below it the day before. If <local>=True, assumes that <dir> is a string containing a path to a directory containing stock data. Files in this directory should be .csv's and have filename equal to the stock symbol whose data they are holding. This method assumes that the file <symbol>.csv exists in <dir>, if <local>=True. Prints a message to the console using <listener> and returns an empty list if not enough data could be found to compute the necessary values. """ try: macd = MACD(symbol, start, end, local=local, dir=dir) except NotEnoughDataError as e: msg = Message() msg.add_line(str(e)) listener.send(msg) return [] # Get a 200-day EMA of price data = get_data(symbol, start, end, local=local, dir=dir) if data is None: return [] try: EMA(data, "Adj Close", "EMA", 200) except NotEnoughDataError as e: msg = Message() msg.add_line(str(e)) listener.send(msg) signals = [] for i in range(1, len(macd.index)): day_before = macd.index[i - 1] day = macd.index[i] if macd["MACD"][day] > macd["Signal"][day] and macd["MACD"][day_before] <= macd["Signal"][day_before] and \ data["Adj Close"][day] > data["EMA"][day]: signals.append(day) return signals
def save(symbol: str, start: dt.datetime, end: dt.datetime, listener: Listener, dir: str) -> None: """ Saves all price data associated with the given symbol as a .csv file in the directory given by <dir> The .csv file shares the same name as the symbol, and if a file already exists with that name it will be overwritten """ df = get_data(symbol, start, end) if df is None: msg = Message() msg.add_line("*****************************") msg.add_line(f"KeyError fetching data for {symbol}") msg.add_line("*****************************") listener.send(msg) return f = open(dir + "/" + symbol + ".csv", "w") df.to_csv(f)
def check_for_crosses(lst: SyncedList, listener: Listener, start: datetime.datetime, end: datetime.datetime): """ Check for crosses of the <short>-day moving average above the <long>-day moving average in the last five days of trading. <lst> should be a SyncedList of stock symbols, and <listener> is what the threads spawned to accomplish the task will use to communicate with the console. """ msg = Message() symbol = lst.pop() while symbol is not None: msg.add_line("Analyzing symbol...") listener.send(msg) msg.reset() crosses = golden_cross(symbol, listener, SHORT_MOVING_AVERAGE, LONG_MOVING_AVERAGE, start, end) for cross in crosses: msg.add_line("=========================================") msg.add_line(f"Cross above for {symbol} on {cross}") listener.send(msg) msg.reset() symbol = lst.pop()
def golden_cross(symbol: str, listener: Listener, short: int, long: int, start: datetime.datetime, end: datetime.datetime, local=False, dir="") -> List[datetime.datetime]: """ Check for crosses by the <short>-day moving average above the <long>-day moving average some time between <start> and <end>. The returned list contains all days on which the short-term average closed ABOVE the long-term average, having been below the long-term average on the previous day. If <local>=True, assumes that <dir> is a string containing a path to a directory containing stock data. Files in this directory should be .csv's and have filename equal to the stock symbol whose data they are holding. This method assumes that the file <symbol>.csv exists in <dir>, if <local>=True. """ msg = Message() try: short_average = moving_average(symbol, start, end, short, local=local, dir=dir) long_average = moving_average(symbol, start, end, long, local=local, dir=dir) except NotEnoughDataError as e: msg.add_line(str(e)) listener.send(msg) return [] if short_average is not None and long_average is not None: days = short_average.index crosses = [] for i in range(len(days) - 1): if (short_average["Average"][days[i]] <= long_average["Average"][days[i]] and short_average["Average"][days[i + 1]] > long_average["Average"][days[i + 1]]): crosses.append(days[i + 1]) return crosses else: msg.add_line("******************************************************") msg.add_line(f"Couldn't get data for {symbol}") msg.add_line("******************************************************") listener.send(msg) return []
def check_overbought_oversold(symbols: SyncedList, start: datetime.datetime, end: datetime.datetime, period: int, listener: Listener): symbol = symbols.pop() while symbol is not None: msg = Message() msg.add_line("Checking " + symbol + "...") listener.send(msg) df = rsi(symbol, start, end, period) if df is not None: for date in df.index: if df["RSI"][date] >= 80: msg = Message() msg.add_line("=========================") msg.add_line(symbol + " was overbought on " + str(date)) msg.add_line("=========================") listener.send(msg) elif df["RSI"][date] <= 20: msg = Message() msg.add_line("=========================") msg.add_line(symbol + " was oversold on " + str(date)) msg.add_line("=========================") listener.send(msg) symbol = symbols.pop()
def analyze(symbol: str, tracker: IndicatorResultTracker, days: int, listener: Listener): """ Analyzes the past 10 years of price data of the given stock symbol. Computes the standard 14-day period RSI for the interval ranging from January 1st, 2010 to December 31, 2019. Then, searches for periods where the stock was "oversold"; for our purposes, that means an RSI of 20 or less. Searches for periods containing some number of consecutive days on which the RSI remained below 20, followed immediately by a day on which the RSI was above 20. Examines the closing prices of the 10 days following this latter day, to see how frequently price increases follow a period of "oversold"-ness, and what the average price change is 1,2,3,... and 10 days after a period of "oversold"-ness. Uses the above class, IndicatorResultTracker, to keep track of the relevant data. """ msg = Message() # msg.add_line(f"Analyzing {symbol}") # listener.send(msg) df_rsi = rsi(symbol, START_DATE, END_DATE, RSI_PERIOD, local=True, dir="../data") last_was_oversold = False if df_rsi is not None: for i in range(len(df_rsi.index)): date = df_rsi.index[i] if df_rsi["RSI"][date] <= 20: if not last_was_oversold: last_was_oversold = True msg.add_line("=====================================") msg.add_line(f"{symbol} was oversold on {date}") msg.add_line("=====================================") else: if last_was_oversold: listener.send(msg) msg.reset() last_was_oversold = False tracker.record_occurrence() listener.send(msg) msg.reset() j = i + 1 while j <= i + days and j < len(df_rsi.index): change = (df_rsi["Adj Close"][df_rsi.index[j]] - df_rsi["Adj Close"][df_rsi.index[i]] ) / df_rsi["Adj Close"][df_rsi.index[i]] msg.add_line( f"Recording change for {symbol} of {format(change, '.5f')} on day {j-i}" ) tracker.record_change(change, j - i) j += 1 listener.send(msg) msg.reset()
def analyze_symbols(symbols: SyncedList, days: int): threads = [] tracker = IndicatorResultTracker(days) listener = Listener() for i in range(6): x = threading.Thread(target=_analyze_thread, args=(symbols, tracker, days, listener)) x.start() threads.append(x) for thread in threads: thread.join() tracker.summarize()
def check_for_MACD_signal_crosses(lst: SyncedList, listener: Listener, start: datetime.datetime, end: datetime.datetime): """ Check each stock symbol in the given SyncedList for crosses of the MACD above its signal line while price is above its 200-day EMA during the period from <start> to <end>. Print a message to the console for each such cross found. """ symbol = lst.pop() msg = Message() while symbol is not None: msg.add_line(f"Analyzing {symbol}...") listener.send(msg) msg.reset() signals = MACD_signal(symbol, listener, start, end, local=True, dir="../data") for signal in signals: msg.add_line("======================================") msg.add_line(f"Signal on {signal} for {symbol}") msg.add_line("======================================") listener.send(msg) signal_output.save(msg) msg.reset() symbol = lst.pop()
def analyze_symbols(lst: SyncedList, start: datetime.datetime, end: datetime.datetime, func: Callable[[SyncedList, Listener, datetime.datetime, datetime.datetime], None]) -> \ List[threading.Thread]: """ Spawns NUM_THREADS threads to analyze the given list of symbols. <func> is the method that each thread will run. It's signature should match the above. It should take a SyncedList of symbols to analyze, a Listener object with which to communicate with the console, and two dates: a start date for analysis and an end date for analysis. It should return nothing. """ listener = Listener() threads = [] # Start NUM_THREADS different threads, each drawing from the same central list of symbols, to look for golden # crosses for i in range(NUM_THREADS): x = threading.Thread(target=func, args=(lst, listener, start, end)) x.start() threads.append(x) return threads
except FileExistsError: print(f"Folder \"{sys.argv[1]}\" already exists. Continuing...") try: source = open(sys.argv[2]) except OSError: print(f"Couldn't open source file {sys.argv[2]}") sys.exit(1) symbols = [] for symbol in source: symbol = symbol.strip() if symbol.isalpha(): symbols.append(symbol) synced = SyncedList(symbols) threads = [] for i in range(2): x = threading.Thread(target=save_symbols, args=(synced, dt.datetime(2010, 1, 1), dt.datetime.today(), Listener(), sys.argv[1])) x.start() threads.append(x) for thread in threads: thread.join() end = time.time() print("Time taken: " + str(end - start))
listener.send(msg) df = rsi(symbol, start, end, period) if df is not None: for date in df.index: if df["RSI"][date] >= 80: msg = Message() msg.add_line("=========================") msg.add_line(symbol + " was overbought on " + str(date)) msg.add_line("=========================") listener.send(msg) elif df["RSI"][date] <= 20: msg = Message() msg.add_line("=========================") msg.add_line(symbol + " was oversold on " + str(date)) msg.add_line("=========================") listener.send(msg) symbol = symbols.pop() if __name__ == '__main__': symbols = [] f = open("../s&p500_symbols.txt") for line in f: line = line.strip() symbols.append(line) lst = SyncedList(symbols) check_overbought_oversold(lst, datetime.datetime(2010, 1, 1), datetime.datetime(2019, 12, 31), 14, Listener())