def test_to_string_should_return_date_string(self) -> None: # ARRANGE none_case: date = None invalid_type_case: int = 1 date_case: date = date(2000, 1, 1) # ACT ret_none_case: str = DateUtils.to_string(none_case) ret_invalid_type_case: str = DateUtils.to_string(invalid_type_case) ret_date_case: str = DateUtils.to_string(date_case) # ASSERT self.assertEqual('', ret_none_case) self.assertEqual('', ret_invalid_type_case) self.assertEqual('2000-01-01', ret_date_case)
def is_tmrw_valid(self) -> bool: tmrw = date.today() + timedelta(days=1) tmrw_as_string: str = DateUtils.to_string(tmrw) calendars: List[Calendar] = self.__api.get_calendar( tmrw_as_string, tmrw_as_string) LogUtils.debug(calendars) return (calendars[0].date.to_pydatetime().date() == tmrw)
def run(self, req: BackTestRunRequest) -> BackTestRunResponse: if not req or not req.is_valid_model(): raise BadRequestException() response: BackTestRunResponse = BackTestRunResponse(req) # Init Symbols symbols: List[SymbolMaster] = self.__get_symbols__(req) # Init Prices prices: DataFrame = self.__get_prices__(req, symbols) # Do Base Preparation prices[AppConsts.CUSTOM_COL_PV] = prices[ AppConsts.PRICE_COL_CLOSE] * prices[AppConsts.PRICE_COL_VOLUME] for s in symbols: prices = self.__calc_service.append_sma( prices=prices, index=[s.id], sma_period=AppConsts.ADV_PERIOD_DFLT, sma_column_name=AppConsts.CUSTOM_COL_ADV, target_column=AppConsts.PRICE_COL_VOLUME) prices = self.__calc_service.append_sma( prices=prices, index=[s.id], sma_period=AppConsts.ADPV_PERIOD_DFLT, sma_column_name=AppConsts.CUSTOM_COL_ADPV, target_column=AppConsts.CUSTOM_COL_PV) LogUtils.debug('Prices shape after base preparation={0}'.format( prices.shape)) # region Init Service strategy_service: Any = self.__stock_service.get_strategy_service( req.strategy_type, req.strategy_request, symbols, prices) if not strategy_service or not strategy_service._is_valid_request(): raise BadRequestException() strategy_service._do_preparations() LogUtils.debug('Prices shape after strategy preparation={0}'.format( prices.shape)) # endregion LogUtils.debug(prices.info()) # region Init Dates start_date: date = DateUtils.add_business_days(req.date_from_obj, -1) start_date = DateUtils.add_business_days(start_date, 1) start_date_str: str = DateUtils.to_string(start_date) end_date: date = DateUtils.add_business_days(req.date_to_obj, -1) dates: DataFrame = self.__stock_service.get_dates( prices, start_date, end_date) LogUtils.debug( 'Dates actual_start={0}, actual_end={1}, shape={2}'.format( start_date, end_date, dates.shape)) # endregion # region Loop Dates strategy_item: BackTestResultItem = next( b for b in response.back_test_result_items if b.target == req.strategy_type) strategy_item.capital[start_date_str] = req.start_capital strategy_item.capital_available[start_date_str] = req.start_capital portfolio: Dict = {} for i, date_row in dates.iterrows(): current_date = date_row[AppConsts.PRICE_COL_DATE] current_date_str: str = DateUtils.to_string(current_date) next_date = date_row[AppConsts.CUSTOM_COL_NEXT_DATE] next_date_str = DateUtils.to_string(next_date) next_next_date = date_row[AppConsts.CUSTOM_COL_NEXT_NEXT_DATE] shuffle(symbols) for symbol in symbols: has_price: bool = (symbol.id, current_date) in prices.index if not has_price: continue price: Series = prices.loc[symbol.id, current_date] if symbol.instrument == AppConsts.INSTRUMENT_ETF: # region Benchmark b_result_item: BackTestResultItem = next( b for b in response.back_test_result_items if b.target == symbol.symbol) if not b_result_item: continue if not b_result_item.transactions: no_of_shares: int = self.__stock_service.get_no_of_shares( req.start_capital, req.pct_risk_per_trade, req.volume_limit, price, req.slippage) if no_of_shares == 0: LogUtils.warning('0 shares for ETF={0}'.format( symbol.symbol)) continue b_transaction: Transaction = Transaction() b_transaction.symbol_master = symbol b_transaction.action = AppConsts.ACTION_BUY b_transaction.start_date = current_date b_transaction.start_price = price.loc[ AppConsts.PRICE_COL_OPEN] b_transaction.no_of_shares = no_of_shares b_result_item.transactions.append(b_transaction) b_result_item.capital[ current_date_str] = req.start_capital else: b_transaction: Transaction = b_result_item.transactions[ 0] b_transaction.end_date = current_date b_transaction.end_price = price.loc[ AppConsts.PRICE_COL_CLOSE] b_transaction.set_readonly_props() b_result_item.capital[ current_date_str] = self.__calc_benchmark_capital( req, b_transaction.start_price, b_transaction.end_price, b_transaction.no_of_shares) b_result_item.ttl_no_days += 1 # endregion else: # region Strategy strategy_service._do_calculations(symbol.id, current_date) action: str = strategy_service._get_action() is_in_position: bool = symbol.id in portfolio if not is_in_position: if len(portfolio ) == req.portfolio_max: # todo: prioritize? continue if current_date == end_date or next_date >= end_date: continue has_next_price: bool = (symbol.id, next_date) in prices.index has_next_next_price: bool = ( symbol.id, next_next_date) in prices.index if not has_next_price or not has_next_next_price: continue adv: float = price.loc[ AppConsts.CUSTOM_COL_ADV] if price.loc[ AppConsts.CUSTOM_COL_ADV] > 0 else price.loc[ AppConsts.PRICE_COL_VOLUME] if adv < req.adv_min: continue adpv: float = price.loc[ AppConsts.CUSTOM_COL_ADPV] if price.loc[ AppConsts.CUSTOM_COL_ADPV] > 0 else price.loc[ AppConsts.CUSTOM_COL_PV] if adpv < req.adpv_min: continue next_price: Series = prices.loc[symbol.id, next_date] has_entry_conditions: bool = strategy_service._has_entry_conditions( symbol.id, current_date) if has_entry_conditions: no_of_shares: int = self.__stock_service.get_no_of_shares( strategy_item. capital_available[current_date_str], req.pct_risk_per_trade, req.volume_limit, next_price, req.slippage, action == AppConsts.ACTION_BUY) if no_of_shares == 0: continue trans: Transaction = Transaction() trans.symbol_master = symbol trans.action = action trans.start_date = next_date trans.start_price = next_price.loc[ AppConsts.PRICE_COL_OPEN] trans.no_of_shares = no_of_shares trans_amount: float = NumberUtils.round( trans.start_price * no_of_shares) strategy_item.capital_available[ current_date_str] -= trans_amount # Add to portfolio portfolio[symbol.id] = trans elif is_in_position: has_exit_conditions: bool = strategy_service._has_exit_conditions( symbol.id, current_date) has_next_next_price: bool = ( symbol.id, next_next_date) in prices.index if next_date == end_date or not has_next_next_price or has_exit_conditions: next_price: Series = prices.loc[symbol.id, next_date] next_open_price: float = next_price.loc[ AppConsts.PRICE_COL_OPEN] slippage_price: float = 0 if action == AppConsts.ACTION_BUY: slippage_price: float = NumberUtils.round( next_open_price - (next_open_price * AppConsts.BASIS_POINT * req.slippage)) else: slippage_price: float = NumberUtils.round( next_open_price + (next_open_price * AppConsts.BASIS_POINT * req.slippage)) trans: Transaction = portfolio.get(symbol.id) trans.end_date = next_date trans.end_price = slippage_price trans.set_readonly_props() strategy_item.transactions.append(trans) if action == AppConsts.ACTION_BUY: trans_amount = NumberUtils.round( trans.end_price * trans.no_of_shares) strategy_item.capital_available[ current_date_str] += trans_amount else: init_trans_amount = NumberUtils.round( trans.start_price * trans.no_of_shares) strategy_item.capital_available[ current_date_str] += init_trans_amount strategy_item.capital_available[ current_date_str] += trans.change_in_capital # Remove from portfolio portfolio.pop(symbol.id, None) # endregion # capital = capital available + capital in portfolio capital: float = strategy_item.capital_available[current_date_str] for key, val in portfolio.items(): price: Series = prices.loc[key, current_date] capital += price.loc[ AppConsts.PRICE_COL_CLOSE] * val.no_of_shares strategy_item.capital[current_date_str] = NumberUtils.round( capital) strategy_item.ttl_no_days += 1 strategy_item.capital[next_date_str] = strategy_item.capital[ current_date_str] strategy_item.capital_available[ next_date_str] = strategy_item.capital_available[ current_date_str] # endregion for result_item in response.back_test_result_items: result_item.set_readonly_props() return response
def close_positions(self) -> int: errors: List[Exception] = [] try: is_tmrw_valid: bool = self.__alpaca_client.is_tmrw_valid() if not is_tmrw_valid: LogUtils.warning('Tmrw is not a valid trade date') raise BadRequestException('Date', DateUtils.to_string(date.today())) req: GetTradeOrdersRequest = GetTradeOrdersRequest() req.exact_status = AppConsts.ORDER_STATUS_IN_POSITION orders: List[TradeOrderCustom] = self.get_trade_orders(req) if not orders: LogUtils.debug('No positions') for order in orders: try: LogUtils.debug('Check position for = {0}'.format( order.symbol_master.symbol)) spd: StockPriceDaily = self.__stock_service.get_last_single_stock_price_daily( order.symbol_master.id) if not spd: LogUtils.warning('No Stock Price Found') raise NotFoundException('SPD', 'symbol_id', order.symbol_master.id) LogUtils.debug('Last close price = {0}'.format( spd.close_price)) # is_exit: bool = (spd.close_price > order.trade_order.target_price # or spd.close_price < order.trade_order.stop_loss) if True: # To-do: fix this to use strategy service. is_exit: LogUtils.debug('Close position for = {0}'.format( order.symbol_master.symbol)) resp: Order = self.__alpaca_client.submit_order( symbol=order.symbol_master.symbol, qty=order.trade_order.actual_qty, action=AppConsts.ACTION_SELL if order.trade_order.action == AppConsts.ACTION_BUY else AppConsts.ACTION_BUY) if resp: org: TradeOrder = BaseService._get_by_id( TradeOrder, order.trade_order.id) if not org: LogUtils.warning('Order not found.') raise NotFoundException( 'TradeOrder', 'id', order.trade_order.id) org.exit_alpaca_id = resp.id org.status = AppConsts.ORDER_STATUS_SUBMITTED_EXIT org.modified = datetime.now() BaseService._update() else: raise Exception('Close Position Error.') except Exception as ex: LogUtils.error('Close Position Error', ex) errors.append(ex) except Exception as ex: LogUtils.error('Close Position Error', ex) errors.append(ex) finally: self.__email_client.send_html( subject=AppConsts.EMAIL_SUBJECT_CLOSE_POSITIONS, template_path=AppConsts.TEMPLATE_PATH_CLOSE_POSITIONS, model={'errors': errors}) return 1
def queue_positions(self) -> int: errors: List[Exception] = [] try: is_tmrw_valid: bool = self.__alpaca_client.is_tmrw_valid() if not is_tmrw_valid: LogUtils.warning('Tmrw is not a valid trade date') raise BadRequestException('Date', DateUtils.to_string(date.today())) req: GetTradeOrdersRequest = GetTradeOrdersRequest() # If Sunday, check Friday's price. today: date = date.today() if today.weekday() == AppConsts.WEEKDAY_IDX_SUN: today = DateUtils.add_business_days(today, -1) req.created = today.strftime('%Y-%m-%d') req.exact_status = AppConsts.ORDER_STATUS_INIT orders: List[TradeOrderCustom] = self.get_trade_orders(req) req_to_ignore: GetTradeOrdersRequest = GetTradeOrdersRequest() req_to_ignore.status = [ AppConsts.ORDER_STATUS_SUBMITTED_ENTRY, AppConsts.ORDER_STATUS_IN_POSITION, AppConsts.ORDER_STATUS_SUBMITTED_EXIT, AppConsts.ORDER_STATUS_CANCELLED_EXIT ] orders_to_ignore: List[TradeOrderCustom] = self.get_trade_orders( req_to_ignore) symbols_to_ignore: List[str] = [ o.symbol_master.symbol for o in orders_to_ignore ] if orders_to_ignore else [] LogUtils.debug('symbols_to_ignore = {0}'.format(symbols_to_ignore)) if not orders: LogUtils.debug('No orders suggested') shuffle(orders) prioritized_orders: List[TradeOrderCustom] = [] for order in orders: if order.trade_order.strategy == AppConsts.STRATEGY_DOUBLE_BOTTOMS: prioritized_orders.append(order) for order in orders: if order.trade_order.strategy == AppConsts.STRATEGY_DOUBLE_TOPS: prioritized_orders.append(order) accnt: Account = self.__alpaca_client.get_account() capital: float = NumberUtils.to_floor( NumberUtils.to_float(accnt._raw['buying_power']) / 2) # 2 to trade everyday for order in prioritized_orders: try: LogUtils.debug('Try symbol = {0}'.format( order.symbol_master.symbol)) if order.symbol_master.symbol in symbols_to_ignore: LogUtils.debug('Ignore for = {0}'.format( order.symbol_master.symbol)) continue cost: float = NumberUtils.to_float( order.stock_price_daily.close_price * order.trade_order.qty) if cost > capital: LogUtils.debug('Too expensive for = {0}'.format( order.symbol_master.symbol)) continue capital = capital - cost resp: Order = self.__alpaca_client.submit_order( symbol=order.symbol_master.symbol, qty=order.trade_order.qty, action=order.trade_order.action) if resp: org: TradeOrder = BaseService._get_by_id( TradeOrder, order.trade_order.id) if not org: raise NotFoundException('TradeOrder', 'id', order.trade_order.id) org.alpaca_id = resp.id org.status = AppConsts.ORDER_STATUS_SUBMITTED_ENTRY org.order_type = AppConsts.ORDER_TYPE_MARKET org.time_in_force = AppConsts.TIME_IN_FORCE_DAY org.modified = datetime.now() BaseService._update() except Exception as ex: LogUtils.error('Queue Position Error', ex) errors.append(ex) except Exception as ex: LogUtils.error('Queue Position Error', ex) errors.append(ex) finally: self.__email_client.send_html( subject=AppConsts.EMAIL_SUBJECT_QUEUE_POSITIONS, template_path=AppConsts.TEMPLATE_PATH_QUEUE_POSITIONS, model={'errors': errors}) return 1