def get_no_of_shares(self, capital: float, pct_risk_per_trade: float, volume_limit: float, price: Series, slippage: int = None, is_slip_up: bool = True) -> int: if not AppConsts.PRICE_COL_OPEN in price.index \ or not AppConsts.CUSTOM_COL_ADV in price.index: return 0 open_price: float = price.loc[AppConsts.PRICE_COL_OPEN] if slippage: if is_slip_up: open_price = NumberUtils.round(open_price + ( open_price * AppConsts.BASIS_POINT * slippage)) else: open_price = NumberUtils.round(open_price - ( open_price * AppConsts.BASIS_POINT * slippage)) no_of_shares: int = NumberUtils.to_floor(capital * pct_risk_per_trade / 100 / open_price) adv: float = price.loc[AppConsts.CUSTOM_COL_ADV] if price.loc[ AppConsts.CUSTOM_COL_ADV] > 0 else price.loc[ AppConsts.PRICE_COL_VOLUME] max_volume: float = NumberUtils.to_int(adv * volume_limit / 100) if no_of_shares > max_volume: LogUtils.warning( 'Capping max_volume adv={0}, no_of_shares={1}, max_volume={2}'. format(adv, no_of_shares, max_volume)) no_of_shares = max_volume return no_of_shares
def import_prices(self, limit: int = 100) -> int: results: Dict = { 'stock_prices': [], 'etf_prices': [], 'missing_symbols': [], 'errors': [] } try: symbol_masters: List[SymbolMaster] = self.__stock_service.get_symbols(instrument='', exclude_status=[AppConsts.SYMBOL_STATUS_ARCHIVED]) symbols: List[str] = [s.symbol for s in symbol_masters] price_set: BarSet = self.__alpaca_client.get_prices(symbols, limit) if not price_set: raise NotFoundException('BarSet', 'symbols', '') for symbol in symbol_masters: LogUtils.debug('start {0}'.format(symbol.symbol)) prices: Bars = price_set[symbol.symbol] if not prices: LogUtils.warning('{0} price not found.'.format(symbol)) results['missing_symbols'].append(symbol.symbol) continue for price in prices: price_date: datetime = price.t.to_pydatetime() data: tuple = ( symbol.id, price_date, NumberUtils.to_float(price.o), NumberUtils.to_float(price.h), NumberUtils.to_float(price.l), NumberUtils.to_float(price.c), NumberUtils.to_int(price.v), ) if symbol.instrument == AppConsts.INSTRUMENT_STOCK: org: StockPriceDaily = self.__stock_service.get_single_stock_price_daily(symbol.id, price_date) if not org: results['stock_prices'].append(data) else: org: EtfPriceDaily = self.__stock_service.get_single_etf_price_daily(symbol.id, price_date) if not org: results['etf_prices'].append(data) if results['stock_prices']: BaseService._insert_bulk(StockPriceDaily, results['stock_prices']) if results['etf_prices']: BaseService._insert_bulk(EtfPriceDaily, results['etf_prices']) except NotFoundException as ex: results['errors'].append(ex) except Exception as ex: results['errors'].append(ex) finally: self.__email_client.send_html( subject=AppConsts.EMAIL_SUBJECT_IMPORT_PRICES, template_path=AppConsts.TEMPLATE_PATH_IMPORT_PRICES, model=results) if results['errors']: for error in results['errors']: LogUtils.error('Import Price Error', error) return 1
def get_dict(item: Any) -> Dict: ret: Dict = {} if not item: return ret for key, val in vars(item).items(): key = key.lstrip('_') if isinstance(val, int): ret[key] = NumberUtils.to_int(val) elif isinstance(val, (Decimal, float)): ret[key] = NumberUtils.to_float(val) elif isinstance(val, (bool, np.bool_)): ret[key] = bool(val) elif isinstance(val, (datetime.date, datetime.datetime)): ret[key] = val.isoformat() elif isinstance(val, list): ret[key] = ModelUtils.get_dicts(val) elif isinstance(val, Dict): ret[key] = val elif isinstance(val, db.Model): ret[key] = ModelUtils.get_dict(val) elif isinstance(val, (BaseResponse, Entity)): ret[key] = ModelUtils.get_dict(val) else: ret[key] = str(val) return ret
def import_from_csv_yahoo(self) -> str: BaseService._truncate(EPD) files: List[str] = FileUtils.get_files(AppConsts.ETF_PRICE_FOLDER, is_full=True) for file in files: symbol: str = FileUtils.get_wo_ext(FileUtils.get_base_name(file)) org_symbol: SM = self.__stock_service.get_symbol( symbol, AppConsts.INSTRUMENT_ETF) if not org_symbol: continue records: List[Dict] = CsvUtils.parse_as_dict(file) models: List[Any] = [] for record in records: models.append(( org_symbol.id, DateUtils.get_date(record.get(AppConsts.YAHOO_KEY_DATE), AppConsts.YAHOO_DATE_FORMAT), NumberUtils.to_float(record.get(AppConsts.YAHOO_KEY_OPEN)), NumberUtils.to_float(record.get(AppConsts.YAHOO_KEY_HIGH)), NumberUtils.to_float(record.get(AppConsts.YAHOO_KEY_LOW)), NumberUtils.to_float(record.get( AppConsts.YAHOO_KEY_CLOSE)), NumberUtils.to_int(record.get(AppConsts.YAHOO_KEY_VOLUME)), )) BaseService._insert_bulk(EPD, models) return "1"
def set_readonly_props(self) -> None: if not self._transactions: return self._start_capital = self._capital.get( ModelUtils.get_first_key(self._capital)) self._end_capital = self._capital.get( ModelUtils.get_last_key(self._capital)) self._hold_length_days_stats = StatUtils.get_descriptive_stats( [t.hold_length_days for t in self._transactions]) self._change_in_capital_stats = StatUtils.get_descriptive_stats( [t.change_in_capital for t in self._transactions]) self._has_profit_stats = StatUtils.get_descriptive_stats( [NumberUtils.to_int(t.has_profit) for t in self._transactions]) self._pct_return = NumberUtils.get_change(self._end_capital, self._start_capital) self._best_transactions = [ t for t in sorted(self._transactions, key=lambda x: x.change_in_capital, reverse=True) if t.has_profit ][:20] self._worst_transactions = [ t for t in sorted(self._transactions, key=lambda x: x.change_in_capital) if not t.has_profit ][:20] symbol_grouped: Dict = {} for t in self._transactions: if not t.symbol_master.symbol in symbol_grouped: symbol_grouped[t.symbol_master.symbol]: Dict = { 'symbol_master': t.symbol_master, 'change_in_capital': 0, 'no_of_transactions': 0 } symbol_grouped[t.symbol_master. symbol]['change_in_capital'] += t.change_in_capital symbol_grouped[t.symbol_master.symbol]['no_of_transactions'] += 1 symbol_grouped_list: List[BackTestResultItemPerSymbol] = [] for k, v in symbol_grouped.items(): item: BackTestResultItemPerSymbol = BackTestResultItemPerSymbol() item.symbol_master = symbol_grouped[k]['symbol_master'] item.change_in_capital = NumberUtils.round( symbol_grouped[k]['change_in_capital']) item.no_of_transactions = symbol_grouped[k]['no_of_transactions'] symbol_grouped_list.append(item) self._best_symbols = [ i for i in sorted(symbol_grouped_list, key=lambda x: x.change_in_capital, reverse=True) if i.change_in_capital > 0 ][:20] self._worst_symbols = [ i for i in sorted(symbol_grouped_list, key=lambda x: x.change_in_capital) if i.change_in_capital < 0 ][:20]
def get_all_suggestions(self) -> int: error: Exception = None try: accnt: Account = self.get_account() capital: float = NumberUtils.to_floor( NumberUtils.to_float(accnt._raw['buying_power']) / 2) # 2 to trade everyday # Double Bottoms req: TradeSuggestionsRequest = TradeSuggestionsRequest() req.is_job = True req.current_capital = capital req.pct_risk_per_trade = 2.5 req.volume_limit = 0.01 req.test_limit_symbol = 800 req.adv_min = AppConsts.ADV_MIN_DFLT req.adpv_min = AppConsts.ADPV_MIN_DFLT req.strategy_type = AppConsts.STRATEGY_DOUBLE_BOTTOMS req.strategy_request = {} req.strategy_request['exponential_smoothing_alpha'] = 0.8 req.strategy_request['exponential_smoothing_max_min_diff'] = 0.7 req.strategy_request['double_bottoms_diff'] = 1 LogUtils.debug(StringUtils.to_json(req)) self.get_suggestions(req) # Double Tops req: TradeSuggestionsRequest = TradeSuggestionsRequest() req.is_job = True req.current_capital = capital req.pct_risk_per_trade = 2.5 req.volume_limit = 0.01 req.test_limit_symbol = 800 req.adv_min = AppConsts.ADV_MIN_DFLT req.adpv_min = AppConsts.ADPV_MIN_DFLT req.strategy_type = AppConsts.STRATEGY_DOUBLE_TOPS req.strategy_request = {} req.strategy_request['exponential_smoothing_alpha'] = 0.8 req.strategy_request['exponential_smoothing_max_min_diff'] = 0.7 req.strategy_request['double_tops_diff'] = 1 LogUtils.debug(StringUtils.to_json(req)) self.get_suggestions(req) except Exception as ex: error = ex finally: self.__email_client.send_html( subject=AppConsts.EMAIL_SUBJECT_GET_SUGGESTIONS, template_path=AppConsts.TEMPLATE_PATH_GET_SUGGESTIONS, model={'errors': [error] if error else []}) if error: LogUtils.error('Get Suggestions Error', error) return 1
def import_from_csv_filedates(self) -> str: file: str = FileUtils.get_file(AppConsts.INCOME_STMT_FILE) records: List[Dict] = CsvUtils.parse_as_dict(file) for record in records: if not self.__is_valid_intrinio_record_for_filedates(record): continue symbol: str = record.get(AppConsts.INTRINIO_KEY_INC_STMT_TICKER) fiscal_period: str = record.get( AppConsts.INTRINIO_KEY_INC_STMT_FISC_PD) year: int = NumberUtils.to_int( record.get(AppConsts.INTRINIO_KEY_INC_STMT_FISC_YR)) quarter: int = self.__get_quarter(fiscal_period) file_date: date = DateUtils.get_date( record.get(AppConsts.INTRINIO_KEY_INC_STMT_FILE_DTE), AppConsts.INTRINIO_FILE_DTE_FMT) if not file_date or quarter == 4: continue org_symbol: SM = self.__stock_service.get_symbol( symbol, AppConsts.INSTRUMENT_STOCK) if not org_symbol: continue org_fn: FN = self.__stock_service.get_financial( org_symbol.id, year, quarter) if not org_fn: continue org_fn.file_date = file_date BaseService._update() return "1"
def __get_quarter(self, fiscal_period: str) -> int: if StringUtils.isNullOrWhitespace(fiscal_period): return None if fiscal_period.endswith(AppConsts.INTRINIO_PERIOD_SUFFIX_FY): return 4 return NumberUtils.to_int( fiscal_period.replace(AppConsts.INTRINIO_PERIOD_PREFIX, '').replace( AppConsts.INTRINIO_PERIOD_SUFFIX_TTM, ''))
def append_abz( self, prices: DataFrame, index: Any, abz_er_period: int, abz_std_distance: float, abz_constant_k: float, target_column: str = AppConsts.PRICE_COL_CLOSE) -> DataFrame: if not isinstance(prices, DataFrame) \ or not target_column in prices.columns: return None if not AppConsts.CUSTOM_COL_ABZ_PERIOD in prices.columns: prices[AppConsts.CUSTOM_COL_ABZ_PERIOD] = 0 if not AppConsts.CUSTOM_COL_ABZ_MIDDLE in prices.columns: prices[AppConsts.CUSTOM_COL_ABZ_MIDDLE] = 0 if not AppConsts.CUSTOM_COL_ABZ_STD in prices.columns: prices[AppConsts.CUSTOM_COL_ABZ_STD] = 0 if not AppConsts.CUSTOM_COL_ABZ_UPPER in prices.columns: prices[AppConsts.CUSTOM_COL_ABZ_UPPER] = 0 if not AppConsts.CUSTOM_COL_ABZ_LOWER in prices.columns: prices[AppConsts.CUSTOM_COL_ABZ_LOWER] = 0 direction: Series = prices.loc[index][target_column].diff( abz_er_period).abs() volatility: Series = prices.loc[index][target_column].diff().abs( ).rolling(abz_er_period).sum() er: Series = direction / volatility periods: Series = er * abz_constant_k prices[AppConsts.CUSTOM_COL_ABZ_PERIOD].update(periods) cursor: int = 0 for i, row in prices.loc[index].iterrows(): symbol_id: int = i[0] curr_date: date = i[1] period: int = NumberUtils.to_int( row[AppConsts.CUSTOM_COL_ABZ_PERIOD]) period = period if period > 2 else 2 sma_series: Series = prices.loc[index][target_column].rolling( period).mean() std_series: Series = prices.loc[index][target_column].rolling( period).std() sma: float = sma_series[symbol_id, curr_date] std: float = std_series[symbol_id, curr_date] upper: float = sma + (std * abz_std_distance) lower: float = sma - (std * abz_std_distance) prices[AppConsts.CUSTOM_COL_ABZ_MIDDLE].loc[symbol_id, curr_date] = sma prices[AppConsts.CUSTOM_COL_ABZ_UPPER].loc[symbol_id, curr_date] = upper prices[AppConsts.CUSTOM_COL_ABZ_LOWER].loc[symbol_id, curr_date] = lower cursor += 1 return prices
def append_exponential_smoothing_max_min( self, prices: DataFrame, index: Any, exponential_smoothing_max_min_diff: float) -> DataFrame: if not (isinstance(prices, DataFrame) and AppConsts.PRICE_COL_SYMBOL_ID in prices.index.names and AppConsts.PRICE_COL_DATE in prices.index.names and AppConsts.CUSTOM_COL_EXPONENTIAL_SMOOTHING_PRICE in prices.columns and AppConsts.CUSTOM_COL_EXPONENTIAL_SMOOTHING_PRICE_PREV in prices.columns and AppConsts.CUSTOM_COL_EXPONENTIAL_SMOOTHING_PRICE_NEXT in prices.columns): return None if not AppConsts.CUSTOM_COL_EXPONENTIAL_SMOOTHING_MAX in prices.columns: prices[AppConsts.CUSTOM_COL_EXPONENTIAL_SMOOTHING_MAX] = 0 if not AppConsts.CUSTOM_COL_EXPONENTIAL_SMOOTHING_MIN in prices.columns: prices[AppConsts.CUSTOM_COL_EXPONENTIAL_SMOOTHING_MIN] = 0 curr_exp_smoothed_maxmin: float = 0.1 for i, row in prices.loc[index].iterrows(): symbol_id: int = i[0] curr_date: date = i[1] curr_exp_smoothed_p: float = row[ AppConsts.CUSTOM_COL_EXPONENTIAL_SMOOTHING_PRICE] prev_exp_smoothed_p: float = row[ AppConsts.CUSTOM_COL_EXPONENTIAL_SMOOTHING_PRICE_PREV] next_exp_smoothed_p: float = row[ AppConsts.CUSTOM_COL_EXPONENTIAL_SMOOTHING_PRICE_NEXT] maxmin_diff: float = NumberUtils.get_change( curr_exp_smoothed_p, curr_exp_smoothed_maxmin) is_max: bool = ( not np.isnan(prev_exp_smoothed_p) and not prev_exp_smoothed_p == 0 and not np.isnan(next_exp_smoothed_p) and not next_exp_smoothed_p == 0 and maxmin_diff is not None and curr_exp_smoothed_p > prev_exp_smoothed_p and curr_exp_smoothed_p >= next_exp_smoothed_p and abs(maxmin_diff) > exponential_smoothing_max_min_diff) is_min: bool = ( not np.isnan(prev_exp_smoothed_p) and not prev_exp_smoothed_p == 0 and not np.isnan(next_exp_smoothed_p) and not next_exp_smoothed_p == 0 and maxmin_diff is not None and curr_exp_smoothed_p < prev_exp_smoothed_p and curr_exp_smoothed_p <= next_exp_smoothed_p and abs(maxmin_diff) > exponential_smoothing_max_min_diff) prices[AppConsts.CUSTOM_COL_EXPONENTIAL_SMOOTHING_MAX].loc[ symbol_id, curr_date] = 1 if is_max else 0 prices[AppConsts.CUSTOM_COL_EXPONENTIAL_SMOOTHING_MIN].loc[ symbol_id, curr_date] = 1 if is_min else 0 if is_max or is_min: curr_exp_smoothed_maxmin = curr_exp_smoothed_p return prices
def import_from_csv_balancesheets(self) -> str: file: str = FileUtils.get_file(AppConsts.BALANCE_SHEET_FILE) records: List[Dict] = CsvUtils.parse_as_dict(file) for record in records: if not self.__is_valid_intrinio_record(record): continue symbol: str = record.get(AppConsts.INTRINIO_KEY_BLNC_SHEET_TICKER) fiscal_period: str = record.get( AppConsts.INTRINIO_KEY_BLNC_SHEET_FISC_PD) year: int = NumberUtils.to_int( record.get(AppConsts.INTRINIO_KEY_BLNC_SHEET_FISC_YR)) quarter: int = self.__get_quarter(fiscal_period) org_symbol: SM = self.__stock_service.get_symbol( symbol, AppConsts.INSTRUMENT_STOCK) if not org_symbol: continue org_fn: FN = self.__stock_service.get_financial( org_symbol.id, year, quarter) if not org_fn: continue org_fn.current_assets = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_BLNC_SHEET_CURR_ASSETS)) org_fn.ttl_assets = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_BLNC_SHEET_ASSETS)) org_fn.current_liabilities = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_BLNC_SHEET_CURR_LIABS)) org_fn.ttl_liabilities = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_BLNC_SHEET_LIABS)) org_fn.ttl_equity = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_BLNC_SHEET_EQUITY)) BaseService._update() return "1"
def append_double_tops(self, prices: DataFrame, index: Any, price_column: str, smooth_price_column: str, max_column: str, min_column: str, double_tops_diff: float) -> DataFrame: if not (isinstance(prices, DataFrame) and AppConsts.PRICE_COL_SYMBOL_ID in prices.index.names and AppConsts.PRICE_COL_DATE in prices.index.names and price_column in prices.columns and smooth_price_column in prices.columns and max_column in prices.columns and min_column in prices.columns): return None if not AppConsts.CUSTOM_COL_DOUBLE_TOPS in prices.columns: prices[AppConsts.CUSTOM_COL_DOUBLE_TOPS] = 0 if not AppConsts.CUSTOM_COL_DOUBLE_TOPS_TARGET_PRICE in prices.columns: prices[AppConsts.CUSTOM_COL_DOUBLE_TOPS_TARGET_PRICE] = 0 if not AppConsts.CUSTOM_COL_DOUBLE_TOPS_STOP_LOSS in prices.columns: prices[AppConsts.CUSTOM_COL_DOUBLE_TOPS_STOP_LOSS] = 0 last_four_maxmins: List[float] = [] for i, row in prices.loc[index].iterrows(): symbol_id: int = i[0] curr_date: date = i[1] is_max: bool = row[max_column] == 1 is_min: bool = row[min_column] == 1 if is_max or is_min: if len(last_four_maxmins) >= 4: last_four_maxmins.pop(0) last_four_maxmins.append(row[smooth_price_column]) if len(last_four_maxmins) < 4: continue (point_a, point_b, point_c, point_d) = last_four_maxmins point_b_d_diff: float = NumberUtils.get_change(point_b, point_d) target_price: float = point_c - (point_d - point_c) has_double_tops: bool = (point_a < point_b and point_a < point_c and point_a < point_d and point_b > point_c and point_c < point_d and abs(point_b_d_diff) < double_tops_diff and row[price_column] < point_c) # and row[price_column] > target_price) # Actual Double Tops condition, but get better results without it. if has_double_tops: prices[AppConsts.CUSTOM_COL_DOUBLE_TOPS].loc[symbol_id, curr_date] = 1 prices[AppConsts.CUSTOM_COL_DOUBLE_TOPS_TARGET_PRICE].loc[ symbol_id, curr_date] = target_price prices[AppConsts.CUSTOM_COL_DOUBLE_TOPS_STOP_LOSS].loc[ symbol_id, curr_date] = point_d return prices
def import_from_csv_incomestatements(self) -> str: BaseService._truncate(FN) file: str = FileUtils.get_file(AppConsts.INCOME_STMT_FILE) records: List[Dict] = CsvUtils.parse_as_dict(file) models: List[Any] = [] for record in records: if not self.__is_valid_intrinio_record(record): continue symbol: str = record.get(AppConsts.INTRINIO_KEY_INC_STMT_TICKER) fiscal_period: str = record.get( AppConsts.INTRINIO_KEY_INC_STMT_FISC_PD) org_symbol: SM = self.__stock_service.get_symbol( symbol, AppConsts.INSTRUMENT_STOCK) if not org_symbol: continue quarter_end_dte: date = DateUtils.get_date( record.get(AppConsts.INTRINIO_KEY_INC_STMT_END_DTE), AppConsts.INTRINIO_END_DTE_FMT) file_date: date = DateUtils.get_date( record.get(AppConsts.INTRINIO_KEY_INC_STMT_FILE_DTE), AppConsts.INTRINIO_FILE_DTE_FMT) models.append( (org_symbol.id, record.get(AppConsts.INTRINIO_KEY_INC_STMT_FISC_YR), self.__get_quarter(fiscal_period), quarter_end_dte, file_date, None, NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_INC_STMT_TTLREV)), NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_INC_STMT_TTLPROF)), NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_INC_STMT_TTLOPINC)), NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_INC_STMT_NETINC)), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None)) BaseService._insert_bulk(FN, models) return "1"
def set_readonly_props(self) -> None: if not hasattr(self, 'start_date') \ or not hasattr(self, 'end_date') \ or not hasattr(self, 'start_price') \ or not hasattr(self, 'end_price') \ or not hasattr(self, 'no_of_shares') \ or not hasattr(self, 'action'): return delta: timedelta = DateUtils.get_diff(self._end_date, self._start_date) if self._action == AppConsts.ACTION_BUY: self._net_change_in_price = NumberUtils.round(self._end_price - self._start_price) elif self._action == AppConsts.ACTION_SELL: self._net_change_in_price = NumberUtils.round(self._start_price - self._end_price) self._year = self._end_date.year self._quarter = DateUtils.get_quarter(self._end_date.month) self._month = self._end_date.month self._hold_length_days = delta.days if delta else 0 self._change_in_capital = NumberUtils.round(self._net_change_in_price * self._no_of_shares) self._has_profit = (self._change_in_capital > 0)
def get_price_dataframe(self, prices: List[Any]) -> DataFrame: if not prices \ or not isinstance(prices, List) \ or not isinstance(prices[0], (SPD, EPD)): return None df: DataFrame = pd.DataFrame( data=[ [ p.id, p.symbol_id, # idx = 0, 0 p.price_date, # idx = 0, 1 NumberUtils.to_float(p.open_price), NumberUtils.to_float(p.high_price), NumberUtils.to_float(p.low_price), NumberUtils.to_float(p.close_price), p.volume ] for p in prices ], columns=AppConsts.PRICE_COLS) df = df.set_index( [AppConsts.PRICE_COL_SYMBOL_ID, AppConsts.PRICE_COL_DATE]) return df
def _has_entry_conditions(self, symbol_id: int, current_date: date) -> bool: current_row: Series = self._prices.loc[symbol_id, current_date] if NumberUtils.to_float( current_row.loc[AppConsts.CUSTOM_COL_SMA_SLOW]) == 0: return False is_above: bool = (current_row.loc[AppConsts.CUSTOM_COL_SMA_FAST] > current_row.loc[AppConsts.CUSTOM_COL_SMA_SLOW]) if not symbol_id in self._target: self._target[symbol_id] = {} self._target[symbol_id]['is_above'] = is_above return False was_above: bool = self._target[symbol_id]['is_above'] self._target[symbol_id]['is_above'] = is_above return (is_above and not was_above)
def test_has_bit_should_return_appropriately(self) -> None: # ACT zero_has_one: bool = NumberUtils.has_bit(0, 1) one_has_one: bool = NumberUtils.has_bit(1, 1) one_has_two: bool = NumberUtils.has_bit(1, 2) two_has_one: bool = NumberUtils.has_bit(2, 1) two_has_two: bool = NumberUtils.has_bit(2, 2) two_has_four: bool = NumberUtils.has_bit(2, 4) three_has_one: bool = NumberUtils.has_bit(3, 1) three_has_two: bool = NumberUtils.has_bit(3, 2) three_has_four: bool = NumberUtils.has_bit(3, 4) four_has_one: bool = NumberUtils.has_bit(4, 1) four_has_two: bool = NumberUtils.has_bit(4, 2) four_has_four: bool = NumberUtils.has_bit(4, 4) four_has_eight: bool = NumberUtils.has_bit(4, 8) # ASSERT self.assertFalse(zero_has_one) self.assertTrue(one_has_one) self.assertFalse(one_has_two) self.assertFalse(two_has_one) self.assertTrue(two_has_two) self.assertFalse(two_has_four) self.assertTrue(three_has_one) self.assertTrue(three_has_two) self.assertFalse(three_has_four) self.assertFalse(four_has_one) self.assertFalse(four_has_two) self.assertTrue(four_has_four) self.assertFalse(four_has_eight)
def test_delete_bit_should_return_appropriately(self) -> None: # ACT zero_delete_one: bool = NumberUtils.delete_bit(0, 1) zero_delete_two: bool = NumberUtils.delete_bit(0, 2) one_delete_one: bool = NumberUtils.delete_bit(1, 1) one_delete_two: bool = NumberUtils.delete_bit(1, 2) two_delete_one: bool = NumberUtils.delete_bit(2, 1) two_delete_two: bool = NumberUtils.delete_bit(2, 2) two_delete_four: bool = NumberUtils.delete_bit(2, 4) three_delete_one: bool = NumberUtils.delete_bit(3, 1) three_delete_two: bool = NumberUtils.delete_bit(3, 2) three_delete_four: bool = NumberUtils.delete_bit(3, 4) four_delete_one: bool = NumberUtils.delete_bit(4, 1) four_delete_two: bool = NumberUtils.delete_bit(4, 2) four_delete_four: bool = NumberUtils.delete_bit(4, 4) four_delete_eight: bool = NumberUtils.delete_bit(4, 8) # ASSERT self.assertEqual(0, zero_delete_one) self.assertEqual(0, zero_delete_two) self.assertEqual(0, one_delete_one) self.assertEqual(1, one_delete_two) self.assertEqual(2, two_delete_one) self.assertEqual(0, two_delete_two) self.assertEqual(2, two_delete_four) self.assertEqual(2, three_delete_one) self.assertEqual(1, three_delete_two) self.assertEqual(3, three_delete_four) self.assertEqual(4, four_delete_one) self.assertEqual(4, four_delete_two) self.assertEqual(0, four_delete_four) self.assertEqual(4, four_delete_eight)
def start_price(self, val: Any) -> None: self._start_price = NumberUtils.to_float(val)
def end_price(self, val: Any) -> None: self._end_price = NumberUtils.to_float(val)
def __calc_benchmark_capital(self, req: BackTestRunRequest, start_price: float, end_price: float, no_of_shares: int) -> float: capital: float = req.start_capital - (start_price * no_of_shares) return NumberUtils.round(capital + (end_price * no_of_shares))
def append_inverted_head_and_shoulders(self, prices: DataFrame, index: Any, price_column: str, smooth_price_column: str, max_column: str, min_column: str, shoulder_diff: float, neck_diff: float) -> DataFrame: if not (isinstance(prices, DataFrame) and AppConsts.PRICE_COL_SYMBOL_ID in prices.index.names and AppConsts.PRICE_COL_DATE in prices.index.names and price_column in prices.columns and smooth_price_column in prices.columns and max_column in prices.columns and min_column in prices.columns): return None if not AppConsts.CUSTOM_COL_INVERTED_HEAD_AND_SHOULDERS in prices.columns: prices[AppConsts.CUSTOM_COL_INVERTED_HEAD_AND_SHOULDERS] = 0 if not AppConsts.CUSTOM_COL_INVERTED_HEAD_AND_SHOULDERS_TARGET_PRICE in prices.columns: prices[AppConsts. CUSTOM_COL_INVERTED_HEAD_AND_SHOULDERS_TARGET_PRICE] = 0 if not AppConsts.CUSTOM_COL_INVERTED_HEAD_AND_SHOULDERS_STOP_LOSS in prices.columns: prices[ AppConsts.CUSTOM_COL_INVERTED_HEAD_AND_SHOULDERS_STOP_LOSS] = 0 last_six_maxmins: List[float] = [] for i, row in prices.loc[index].iterrows(): symbol_id: int = i[0] curr_date: date = i[1] is_max: bool = row[max_column] == 1 is_min: bool = row[min_column] == 1 if is_max or is_min: if len(last_six_maxmins) >= 6: last_six_maxmins.pop(0) last_six_maxmins.append(row[smooth_price_column]) if len(last_six_maxmins) < 6: continue (point_a, point_b, point_c, point_d, point_e, point_f) = last_six_maxmins point_b_f_diff: float = NumberUtils.get_change(point_b, point_f) point_c_e_diff: float = NumberUtils.get_change(point_c, point_e) neck_point: float = point_c if point_c > point_e else point_e target_price: float = neck_point + (neck_point - point_d) has_inverted_head_and_shoulders: bool = ( point_a > point_b and point_a > point_c and point_a > point_d and point_a > point_e and point_a > point_f and point_b < point_c and point_b > point_d and point_b < point_e and point_c > point_d and point_c > point_f and point_d < point_e and point_d < point_f and abs(point_b_f_diff) < shoulder_diff and abs(point_c_e_diff) < neck_point and row[price_column] > neck_point and row[price_column] < target_price) if has_inverted_head_and_shoulders: prices[AppConsts.CUSTOM_COL_INVERTED_HEAD_AND_SHOULDERS].loc[ symbol_id, curr_date] = 1 prices[ AppConsts. CUSTOM_COL_INVERTED_HEAD_AND_SHOULDERS_TARGET_PRICE].loc[ symbol_id, curr_date] = target_price prices[AppConsts. CUSTOM_COL_INVERTED_HEAD_AND_SHOULDERS_STOP_LOSS].loc[ symbol_id, curr_date] = point_f return prices
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 import_from_csv_calculations(self) -> str: file: str = FileUtils.get_file(AppConsts.FINANCIAL_CALCS_FILE) records: List[Dict] = CsvUtils.parse_as_dict(file) for record in records: if not self.__is_valid_intrinio_record(record): continue symbol: str = record.get(AppConsts.INTRINIO_KEY_CALCS_TICKER) fiscal_period: str = record.get( AppConsts.INTRINIO_KEY_CALCS_FISC_PD) year: int = NumberUtils.to_int( record.get(AppConsts.INTRINIO_KEY_CALCS_FISC_YR)) quarter: int = self.__get_quarter(fiscal_period) org_symbol: SM = self.__stock_service.get_symbol( symbol, AppConsts.INSTRUMENT_STOCK) if not org_symbol: continue org_fn: FN = self.__stock_service.get_financial( org_symbol.id, year, quarter) if not org_fn: continue org_fn.market_cap = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_CALCS_MARK_CAP)) org_fn.revenue_growth = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_CALCS_REV_GRTH)) org_fn.revenue_qq_growth = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_CALCS_REV_QQ_GRTH)) org_fn.nopat_growth = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_CALCS_NOPAT_GRTH)) org_fn.nopat_qq_growth = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_CALCS_NOTPAT_QQ_GRTH)) org_fn.net_income_growth = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_CALCS_INCM_GRTH)) org_fn.net_income_qq_growth = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_CALCS_INCM_QQ_GRTH)) org_fn.free_cash_flow = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_CALCS_CSH_FLOW)) org_fn.current_ratio = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_CALCS_CURR_RATIO)) org_fn.debt_to_equity_ratio = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_CALCS_DE_RATIO)) org_fn.pe_ratio = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_CALCS_PE_RATIO)) org_fn.pb_ratio = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_CALCS_PB_RATIO)) org_fn.div_payout_ratio = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_CALCS_DIV_PAYOUT_RATIO)) org_fn.roe = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_CALCS_ROE)) org_fn.roa = NumberUtils.to_float( record.get(AppConsts.INTRINIO_KEY_CALCS_ROA)) BaseService._update() return "1"
def sync_orders(self) -> int: errors: List[Exception] = [] try: req: GetTradeOrdersRequest = GetTradeOrdersRequest() req.status = [ AppConsts.ORDER_STATUS_SUBMITTED_ENTRY, AppConsts.ORDER_STATUS_SUBMITTED_EXIT ] orders: List[TradeOrderCustom] = self.get_trade_orders(req) if not orders: LogUtils.debug('No orders submitted') for order in orders: try: LogUtils.debug('Sync order for = {0}'.format( order.symbol_master.symbol)) resp: Order = None if order.trade_order.status == AppConsts.ORDER_STATUS_SUBMITTED_ENTRY: resp = self.__alpaca_client.get_order( order.trade_order.alpaca_id) elif order.trade_order.status == AppConsts.ORDER_STATUS_SUBMITTED_EXIT: resp = self.__alpaca_client.get_order( order.trade_order.exit_alpaca_id) if resp: org: TradeOrder = BaseService._get_by_id( TradeOrder, order.trade_order.id) if not org: raise NotFoundException('TradeOrder', 'id', order.trade_order.id) if order.trade_order.status == AppConsts.ORDER_STATUS_SUBMITTED_ENTRY \ and resp.status == AppConsts.ALPACA_ORDER_STATUS_FILLED: org.status = AppConsts.ORDER_STATUS_IN_POSITION org.actual_qty = NumberUtils.to_int( resp.filled_qty) org.actual_entry_price = NumberUtils.to_float( resp.filled_avg_price) org.modified = datetime.now() BaseService._update() elif order.trade_order.status == AppConsts.ORDER_STATUS_SUBMITTED_ENTRY \ and resp.status == AppConsts.ALPACA_ORDER_STATUS_CANCELLED: org.status = AppConsts.ORDER_STATUS_CANCELLED_ENTRY org.modified = datetime.now() BaseService._update() elif order.trade_order.status == AppConsts.ORDER_STATUS_SUBMITTED_EXIT \ and resp.status == AppConsts.ALPACA_ORDER_STATUS_FILLED: exit_price: StockPriceDaily = self.__stock_service.get_single_stock_price_daily( order.symbol_master.id, DateUtils.get_date( datetime.today().strftime('%Y-%m-%d'), '%Y-%m-%d')) if exit_price: org.exit_stock_price_daily_id = exit_price.id org.status = AppConsts.ORDER_STATUS_COMPLETED org.actual_exit_price = NumberUtils.to_float( resp.filled_avg_price) org.modified = datetime.now() BaseService._update() elif order.trade_order.status == AppConsts.ORDER_STATUS_SUBMITTED_EXIT \ and resp.status == AppConsts.ALPACA_ORDER_STATUS_CANCELLED: org.status = AppConsts.ORDER_STATUS_CANCELLED_EXIT org.modified = datetime.now() BaseService._update() raise Exception('Exit Error = {0}'.format( resp.status)) else: raise Exception('Sync Status = {0}'.format( resp.status)) else: raise NotFoundException('Alpaca Order', 'id', order.trade_order.alpaca_id) except Exception as ex: LogUtils.error('Sync Orders Error', ex) errors.append(ex) except Exception as ex: LogUtils.error('Sync Orders Error', ex) errors.append(ex) finally: self.__email_client.send_html( subject=AppConsts.EMAIL_SUBJECT_SYNC_ORDERS, template_path=AppConsts.TEMPLATE_PATH_SYNC_ORDERS, 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
def test_add_bit_should_return_appropriately(self) -> None: # ACT zero_add_one: bool = NumberUtils.add_bit(0, 1) zero_add_two: bool = NumberUtils.add_bit(0, 2) one_add_one: bool = NumberUtils.add_bit(1, 1) one_add_two: bool = NumberUtils.add_bit(1, 2) two_add_one: bool = NumberUtils.add_bit(2, 1) two_add_two: bool = NumberUtils.add_bit(2, 2) two_add_four: bool = NumberUtils.add_bit(2, 4) three_add_one: bool = NumberUtils.add_bit(3, 1) three_add_two: bool = NumberUtils.add_bit(3, 2) three_add_four: bool = NumberUtils.add_bit(3, 4) four_add_one: bool = NumberUtils.add_bit(4, 1) four_add_two: bool = NumberUtils.add_bit(4, 2) four_add_four: bool = NumberUtils.add_bit(4, 4) four_add_eight: bool = NumberUtils.add_bit(4, 8) # ASSERT self.assertEqual(1, zero_add_one) self.assertEqual(2, zero_add_two) self.assertEqual(1, one_add_one) self.assertEqual(3, one_add_two) self.assertEqual(3, two_add_one) self.assertEqual(2, two_add_two) self.assertEqual(6, two_add_four) self.assertEqual(3, three_add_one) self.assertEqual(3, three_add_two) self.assertEqual(7, three_add_four) self.assertEqual(5, four_add_one) self.assertEqual(6, four_add_two) self.assertEqual(4, four_add_four) self.assertEqual(12, four_add_eight)