def open_position(self): # do not buy if we're out of funds! budget = self.buying_power.spend_and_get_amount() if budget < self.BUDGET_THRESHHOLD: # don't make trades for under a certain threshhold return if self.paper_trading: # if the order timed out or was rejected for some other reason # then try again when next relevant try: self.position = OpenPaperPosition(self.ticker, budget, self.market_data) except TraderbotException as te: print_with_lock("open position exception:", str(te)) self.position = None return else: try: self.position = OpenStockPosition(self.ticker, budget) except TraderbotException as te: print_with_lock("open position exception:", str(te)) self.position = None return # update statistics self.statistics.append({ "open_time": datetime.now(), "quantity": self.position.get_quantity(), "open_price": self.position.get_open_price(), "close_time": -1, "close_price": -1 })
def monitor_order(self, resp, ticker): """Monitors the order with the given ID until it is filled or TIMEOUT is exceeded, in which case the order is cancelled. Returns the response dictionary or throws, depending on whether or not the order succeeded or failed. Checks for fill status twice every second.""" order_id = resp.get('id', None) if order_id is None: raise TraderbotException( "initial order for {} failed".format(ticker)) # before waiting check if we already filled it if resp['state'] == 'filled': return resp # then check every half second for _ in range(2 * self.TIMEOUT): resp = r.orders.get_stock_order_info(order_id) if resp['state'] == 'filled': # TODO orders are being cancelled unexpectedly return resp if resp['state'] == 'cancelled': raise TraderbotException( "order for {} was cancelled unexpectedly: {}".format( ticker, resp)) time.sleep(0.5) # TODO if partially filled here need to tell the caller as much # what the actual qty and price are so they can sell accordingly # same for when they actually sell. maybe return remaining position # so it works for both print_with_lock("TIMING OUT BUT DEBUG IN CASE PARTIAL FILL: ", resp) resp = r.cancel_stock_order(order_id) print_with_lock("debug: response from cancelling stock order: ", resp) raise TraderbotException("order for {} timed out".format(ticker))
def print_close(self, close_price): fqty = self._fnum(self.quantity) ftot = self._fnum(self.quantity * close_price) fnet = self._fnum((close_price - self.open_price) * self.quantity) print_with_lock( "sold {} shares of {} (total of ${}) for a net {} of {}".format( fqty, self.ticker, ftot, "gain" if close_price > self.open_price else "loss", fnet))
def should_buy_on_tick(self): # update both and buy if we have a higher short than long MA self.short_moving_avg.update() self.long_moving_avg.update() print_with_lock("MA for {}: short={} long={}".format( self.ticker, self.short_moving_avg.get_moving_average(), self.long_moving_avg.get_moving_average())) return self.short_moving_avg.get_moving_average( ) > self.long_moving_avg.get_moving_average()
def add_funds(self, amount): """Use this when you close a position to add back the funds earned.""" if not instant: # cannot add funds until 2 days later to non instant accounts return with self.lock.gen_wlock(): self.buying_power += amount print_with_lock("your account now has ${}".format( self.buying_power))
def log_in_to_robinhood(): # only use mfa login if it is enabled mfa_code=None if "mfa-setup-code" in CONFIG.keys(): # gets current mfa code totp = pyotp.TOTP(CONFIG["mfa-setup-code"]).now() print_with_lock("DEBUG: current mfa code:", totp) login = r.login(USERNAME, PASSWORD, mfa_code=mfa_code) print_with_lock("logged in as user {}".format(USERNAME)) return login
def block_until_market_open(): """Block until market open. Does pre-market work as soon as the loop is entered, exactly once.""" time_until_open = get_time_until_market_open() zero_time = timedelta() last_print = time_until_open while time_until_open > zero_time: if last_print - time_until_open > timedelta(hours=1): print_with_lock("still waiting until market open. time remaining:", time_until_open) last_print = time_until_open # update timedelta time_until_open = get_time_until_market_open() print_with_lock("market is open")
def block_until_start_trading(): """Block until the pre-determined time when we will start trading. Market is open at this time, so statistics can be gathered and updated in this loop.""" now = datetime.now() start_of_day_datetime = datetime(now.year, now.month, now.day, START_OF_DAY.hour, START_OF_DAY.minute, START_OF_DAY.second, START_OF_DAY.microsecond) time_until_start_trading = start_of_day_datetime - now zero_time = timedelta() last_print = time_until_start_trading while time_until_start_trading > zero_time: if last_print - time_until_start_trading > timedelta(minutes=5): print_with_lock("market is open. will start trading in: ", time_until_start_trading) last_print = time_until_start_trading time_until_start_trading = start_of_day_datetime - datetime.now() print_with_lock("beginning trading")
def close_position(self): close_price = 0.0 try: close_price = self.position.close() except TraderbotException as te: print_with_lock("close position exception:", str(te)) return ts = datetime.now() qty = self.position.get_quantity() self.buying_power.add_funds(close_price*qty) # update statistics self.statistics[-1]["close_time"] = ts self.statistics[-1]["close_price"] = close_price self.net += ((close_price - self.position.get_open_price()) * self.position.get_quantity()) self.position = None
def run(self): self.init_time = datetime.now() print_with_lock("thread {} began".format(self.ticker)) while self.market_time.is_time_left_to_trade(): self.looking_to_buy() # did we leave the looking to buy function because we bought in? # or because we ran out of resources? if we ran out, end this thread if self.position is None: return # otherwise, we're now looking to sell self.looking_to_sell() # end of the line for us, generate our report self.generate_report()
def __init__(self, market_data, ticker, n, interval='day'): with self.ctor_lock: DayMovingAverage.market_data = market_data # update self with market info, usually pre-market self.n = n self.current_moving_avg = 0.0 self.ticker = ticker # get data for a year then whittle down data_for_last_n_intervals = r.stocks.get_stock_historicals( ticker, interval=interval, span='year') # get the last n intervals from the previous year data_for_last_n_intervals = data_for_last_n_intervals[-n:] # data is currently in the following format: # a list of n dictionaries with the structure: # { # 'begins_at': '2021-03-05T00:00:00Z', # 'close_price': '3000.460000', # 'high_price': '3009.000000', # 'interpolated': False, # 'low_price': '2881.000100', # 'open_price': '3005.000000', # 'session': 'reg', # 'symbol': 'AMZN', # 'volume': 5388551 # } # so we just want the close price to use as our data self.sliding_window = [ float(daily_stats['close_price']) for daily_stats in data_for_last_n_intervals ] self.calculate_moving_average() print_with_lock("{} {} {} moving avg: {}".format( ticker, n, interval, self.current_moving_avg)) # important -- update the sliding window with the current price at the end # this allows us to update the curr moving average once the market opens # by simply replacing the last value in the sliding window with the current # market price self.sliding_window.append( self.market_data.get_data_for_ticker(ticker)) self.sliding_window = self.sliding_window[1:]
def print_data(self): """Pretty printing for the internal data of this object.""" print_with_lock("---- MARKET DATA ----") for ticker, index in self.tickers_to_indices.items(): ticker_data = self.data[index] with ticker_data.lock.gen_rlock(): if len(ticker_data.prices) < ticker_data.trend_len: # make a reversed copy of the list data_snippet = ticker_data.prices[::-1] else: data_snippet = ticker_data.get_last_k_prices_in_order() # data will always be nonempty for a ticker print_with_lock("{}: {} {}".format(ticker, data_snippet[0], data_snippet)) print_with_lock("---------------------")
def print_eod_reports(self): net = 0.0 trades = 0 pp = pprint.PrettyPrinter(indent=4, sort_dicts=False) print_with_lock( "=============================== EOD REPORTS ===============================" ) for report in self.reports: net += report['traderbot net performance'] trades += report['total trades made'] pp.pprint(report) print_with_lock( "===========================================================================" ) print_with_lock( "final summary: {} trades made for a net profit of {}".format( trades, net))
def print(self): with self.lock.gen_rlock(): print_with_lock("TICKERDATA: ind={}, prices={}".format(self.ind, self.prices))
def run_traderbot(): """Main function for this module. Spawns a thread for each ticker that trades on that symbol for the duration of the day.""" # get info from config file and log in global USERNAME, PASSWORD, PAPER_TRADING, TIME_ZONE global START_OF_DAY, END_OF_DAY, TRADE_LIMIT, CONFIG CONFIG = get_json_dict() USERNAME = CONFIG["username"] PASSWORD = CONFIG["password"] MAX_LOSS_PERCENT = CONFIG["max-loss-percent"]/100.0 TAKE_PROFIT_PERCENT = CONFIG["take-profit-percent"]/100.0 SPEND_PERCENT = CONFIG["spend-percent"]/100.0 PAPER_TRADING = CONFIG["paper-trading"] TIME_ZONE = CONFIG.get("time-zone-pandas-market-calendars", "America/New_York") full_start_time_str = CONFIG.get("start-of-day", "09:30") + "={}".format(TIME_ZONE) full_end_time_str = CONFIG.get("end-of-day", "16:00") + "={}".format(TIME_ZONE) START_OF_DAY = datetime.strptime(full_start_time_str, "%H:%M=%Z").time() END_OF_DAY = datetime.strptime(full_end_time_str, "%H:%M=%Z").time() TRADE_LIMIT = CONFIG.get("max-trades-per-day", None) BUDGET = CONFIG.get("budget", None) END_TIME_STR = CONFIG.get("end-time", None) START_TIME_STR = CONFIG.get("start-time", None) ALPACA_KEY = CONFIG["alpaca-api-key"] ALPACA_SECRET_KEY = CONFIG["alpaca-secret-key"] STRATEGIES_DICT = CONFIG["strategies"] HISTORY_SIZE = CONFIG.get("history-len", 16) if not ((HISTORY_SIZE & (HISTORY_SIZE-1) == 0) and HISTORY_SIZE != 0): raise ConfigException("history-len must be a power of two, {} was entered".format(HISTORY_SIZE)) TREND_SIZE = CONFIG.get("trend-len", 3) if TREND_SIZE > HISTORY_SIZE: raise ConfigException("trend-len must be less than or equal to history-len") IS_INSTANT_ACCT = CONFIG.get("instant", False) zero_time = timedelta() login = log_in_to_robinhood() # get list of unique tickers and enforce legality of strategies kv in the config ALL_TICKERS = [] for st in STRATEGIES_DICT: enforce_keys_in_dict(['strategy', 'tickers'], st) enforce_strategy_dict_legal(st['strategy']) ALL_TICKERS.extend(st['tickers']) ALL_TICKERS = list(set(ALL_TICKERS)) # generate parameters so we don't get flagged START_OF_DAY, END_OF_DAY, TRADE_LIMIT = generate_humanlike_parameters() datetime_fmt_str = '%H:%M:%S' if END_TIME_STR is not None: END_OF_DAY = datetime.strptime(END_TIME_STR, datetime_fmt_str).time() if START_TIME_STR is not None: START_OF_DAY = datetime.strptime(START_TIME_STR, datetime_fmt_str).time() print_with_lock("param: will start trading today at:", START_OF_DAY) print_with_lock("param: will stop trading today at:", END_OF_DAY) print_with_lock("param: will make a maximum of {} trades today".format(TRADE_LIMIT)) # busy-spin until market open block_until_market_open() # these variables are shared by each trading thread. they are written by this # main traderbot thread, and read by each trading thread individually market_data = MarketData(ALL_TICKERS, ALPACA_KEY, ALPACA_SECRET_KEY, HISTORY_SIZE, TREND_SIZE) buying_power = BuyingPower(SPEND_PERCENT, IS_INSTANT_ACCT, BUDGET) trade_capper = TradeCapper(TRADE_LIMIT) # now that market open is today, update EOD for time checking now = datetime.now() END_OF_DAY = datetime(now.year, now.month, now.day, END_OF_DAY.hour, END_OF_DAY.minute, END_OF_DAY.second, END_OF_DAY.microsecond) market_time = MarketTime(END_OF_DAY) reports = Reports() # spawn thread for each ticker threads = [] for st in STRATEGIES_DICT: strategy_dict = st['strategy'] tickers = st['tickers'] for ticker in tickers: print_with_lock("initializing thread {} with strategy configuration {}".format(ticker, strategy_dict)) strategy = strategy_factory(strategy_dict, market_data, ticker) if not strategy.is_relevant(): # don't add irrelevant tickers to the threadpool. # long term could figure out how to remove this # from the market data object too continue threads.append(TradingThread(ticker, market_data, market_time, buying_power, trade_capper, strategy, reports, TAKE_PROFIT_PERCENT, MAX_LOSS_PERCENT, PAPER_TRADING)) # busy spin until we decided to start trading block_until_start_trading() # update before we start threads to avoid mass panic market_data.start_stream() market_time.update() # start all threads for t in threads: t.start() # update the timer in the main thread while market_time.is_time_left_to_trade(): market_time.update() # wait for all threads to finish for t in threads: t.join() # now pretty print reports reports.print_eod_reports() # tidy up after ourselves r.logout() print_with_lock("logged out user {}".format(USERNAME))
def print_open(self): fqty = self._fnum(self.quantity) fop = self._fnum(self.open_price) ftot = self._fnum(self.quantity * self.open_price) print_with_lock("opened {} position: {} shares at {} for ${}".format( self.ticker, fqty, fop, ftot))