def __execute_bulk(stmt: str, models: Any = None): LogUtils.debug('START') try: LogUtils.debug(stmt) conn = mysql.connector.connect(host=app_config.DB_HOST, user=app_config.DB_USER_NAME, passwd=app_config.DB_PASSWORD, database=app_config.DB_SCHEMA) cur = conn.cursor() if models: LogUtils.debug('{} records.'.format(len(models))) cur.executemany(stmt, models) else: cur.execute(stmt) conn.commit() except Exception as e: LogUtils.error(e) finally: if cur: cur.close() if conn: conn.close() LogUtils.debug('END')
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 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 parse_as_list(file_path: str) -> List[List[str]]: try: LogUtils.debug('START') if StringUtils.isNullOrWhitespace(file_path): return [] with open(file_path) as f: return list(csv.reader(f)) if f else [] finally: LogUtils.debug('END')
def setUpClass(cls: type) -> None: # This runs once. if not TestInitObj.is_init: LogUtils.debug('START-Unit Tests') TestInitObj.init() # This runs before each test*.py LogUtils.debug('START-Unit Test for {0}'.format(cls.__name__))
def get(self, path: str) -> Any: if not path: return None req = requests.get(path) LogUtils.debug('status code = {0}, path = {1}'.format( req.status_code, path)) if req.status_code != http.HTTPStatus.OK: return None return req.json()
def cancel_order(self, order_id: str) -> None: try: LogUtils.debug('Submit request = {0}'.format(order_id)) self.__api.cancel_order(order_id) except Exception as ex: LogUtils.error( 'Submit Error for {0}'.format(StringUtils.to_json(order)), ex) raise ex
def get_date(datestr: str, fmt: str) -> date: if not isinstance(datestr, str) \ or not isinstance(fmt, str) \ or StringUtils.isNullOrWhitespace(datestr) \ or StringUtils.isNullOrWhitespace(fmt): return None try: return datetime.strptime(datestr, fmt) except Exception as ex: LogUtils.warning('Invalid date inputs={0} {1}'.format( datestr, fmt)) return None
def get_html_table(self, url: str, element_id: str, index_cols: List[str]) -> DataFrame: LogUtils.debug('Url={0}, Element Id={1}'.format(url, element_id)) table: List[Any] = pd.read_html(url, attrs={'id': element_id}, index_col=index_cols) df: DataFrame = table[0] if table else None LogUtils.debug('Df={0}'.format(df)) return df
def import_from_csv_symbols(self) -> str: files: List[str] = FileUtils.get_files(AppConsts.STOCK_PRICE_FOLDER) for file in files: symbol: str = FileUtils.get_wo_ext(file) org_symbol: SM = self.__stock_service.get_symbol( symbol, AppConsts.INSTRUMENT_STOCK) if org_symbol: continue LogUtils.debug('Inserting {0}.'.format(symbol)) BaseService._insert( SM(symbol=symbol, name='', status=AppConsts.SYMBOL_STATUS_INIT, instrument=AppConsts.INSTRUMENT_STOCK)) return "1"
def __get_symbols__(self, req: TradeSuggestionsRequest) -> List[SymbolMaster]: if not req or req.test_limit_symbol <= 0: raise BadRequestException() symbols: List[SymbolMaster] = self.__stock_service.get_symbols( AppConsts.INSTRUMENT_STOCK, [ AppConsts.SYMBOL_STATUS_EXCLUDE_TRADE, AppConsts.SYMBOL_STATUS_ARCHIVED ]) if not symbols: raise DbConnectionException() shuffle(symbols) symbols = symbols[:req.test_limit_symbol] LogUtils.debug('Symbol Count={0}'.format(len(symbols))) return symbols
def __get_prices__(self, symbols: List[SymbolMaster], date_from: date, date_to: date) -> DataFrame: if not symbols: raise BadRequestException() price_items: List[Any] = [] for symbol in symbols: temp: List[ StockPriceDaily] = self.__stock_service.get_stock_prices_daily( symbol.id, date_from, date_to) if temp: price_items.extend(temp) prices: DataFrame = self.__stock_service.get_price_dataframe( price_items) LogUtils.debug('Prices Init Shape={0}'.format(prices.shape)) return prices
def handle(ex: Exception) -> str: response: ResponseModel = ResponseModel() try: LogUtils.error('Handle error', ex) msg: str = str(ex) response.http_status_code = http.HTTPStatus.INTERNAL_SERVER_ERROR response.error_message = msg if msg else 'Error' if isinstance(ex, BadRequestException): response.http_status_code = http.HTTPStatus.BAD_REQUEST if isinstance(ex, NotFoundException): response.http_status_code = http.HTTPStatus.NOT_FOUND if isinstance(ex, DbConnectionException): response.http_status_code = http.HTTPStatus.INTERNAL_SERVER_ERROR except Exception as e: response.http_status_code = http.HTTPStatus.INTERNAL_SERVER_ERROR response.error_message = 'Error' return StringUtils.to_json(response)
def __get_symbols__(self, req: BackTestRunRequest) -> List[SymbolMaster]: if not req or req.test_limit_symbol <= 0: raise BadRequestException() symbols: List[SymbolMaster] = self.__stock_service.get_symbols( AppConsts.INSTRUMENT_STOCK, [AppConsts.SYMBOL_STATUS_EXCLUDE_TRADE]) if not symbols: raise DbConnectionException() shuffle(symbols) symbols = symbols[:req.test_limit_symbol] if req.benchmark_etfs: for benchmark_etf in req.benchmark_etfs: etf_symbol: SymbolMaster = self.__stock_service.get_symbol( benchmark_etf, AppConsts.INSTRUMENT_ETF) if not etf_symbol: continue symbols.append(etf_symbol) LogUtils.debug('Symbol Count={0}'.format(len(symbols))) return symbols
def import_company_profiles(self) -> int: 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] company_profiles: List[Dict] = self.__iex_cloud_client.get_company_profiles(symbols) for profile in company_profiles: symbol: str = profile.get('symbol') name: str = profile.get('companyName') sector: str = profile.get('sector') industry: str = profile.get('industry') org: SymbolMaster = self.__stock_service.get_symbol(symbol, AppConsts.INSTRUMENT_STOCK) if not org: LogUtils.warning('{0} not found.'.format(symbol)) else: LogUtils.debug('Updating {0}'.format(symbol)) org.name = name if name != '' else org.name org.sector = sector if sector != '' else org.sector org.industry = industry if industry != '' else org.industry BaseService._update()
def delete_old_prices(self) -> int: error: Exception = None try: thirty_years_ago: datetime = datetime.now() - timedelta(days=365 * 30) LogUtils.debug( 'Deleting stock price older than {0}'.format(thirty_years_ago)) db.session.query(SPD).filter( SPD.price_date <= thirty_years_ago).delete() db.session.commit() except Exception as ex: error = ex finally: self.__email_client.send_html( subject=AppConsts.EMAIL_SUBJECT_DELETE_PRICES, template_path=AppConsts.TEMPLATE_PATH_DELETE_PRICES, model={'errors': [error] if error else []}) if error: LogUtils.error('Delete Price Error', error) return 1
def get_company_profiles(self, symbols: List[str]) -> List[Dict]: """ https://iexcloud.io/docs/api/#company """ ret: List[Dict] = [] if not symbols: return ret for symbol in symbols: path: str = '/stock/{0}/company'.format(symbol) r = requests.get(self.__get_full_path__(path)) if r.status_code != http.HTTPStatus.OK: LogUtils.debug('{0}={1}', symbol, r.status_code) continue profile: Dict = r.json() if not profile: LogUtils.debug('{0} empty', symbol) else: ret.append(profile) return ret
def import_from_csv_companynames(self) -> str: file: str = FileUtils.get_file(AppConsts.INCOME_STMT_FILE) records: List[Dict] = CsvUtils.parse_as_dict(file) curr_symbol: str = '' for record in records: symbol: str = record.get(AppConsts.INTRINIO_KEY_INC_STMT_TICKER) company_name: str = record.get( AppConsts.INTRINIO_KEY_INC_STMT_NAME) if symbol == curr_symbol or StringUtils.isNullOrWhitespace( company_name): continue curr_symbol = symbol org_symbol: SM = self.__stock_service.get_symbol( curr_symbol, AppConsts.INSTRUMENT_STOCK) if not org_symbol: continue LogUtils.debug('Updating {0}.'.format(company_name)) org_symbol.name = company_name BaseService._update() return "1"
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 submit_order( self, symbol: str, qty: int, action: str, order_type: str = AppConsts.ORDER_TYPE_MARKET, time_in_force: str = AppConsts.TIME_IN_FORCE_DAY) -> Order: ret: Order = None try: ret = self.__api.submit_order(symbol=symbol, qty=qty, side=action, type=order_type, time_in_force=time_in_force) LogUtils.debug('Submit response = {0}'.format(ret)) except Exception as ex: LogUtils.error('Submit Error for {0}'.format(symbol), ex) raise ex return ret
def __get_prices__(self, req: BackTestRunRequest, symbols: List[SymbolMaster]) -> DataFrame: if not req or not symbols: raise BadRequestException() price_items: List[Any] = [] for symbol in symbols: temp: List[Any] = [] if symbol.instrument == AppConsts.INSTRUMENT_STOCK: temp = self.__stock_service.get_stock_prices_daily( symbol.id, req.date_from_obj, req.date_to_obj) elif symbol.instrument == AppConsts.INSTRUMENT_ETF: temp = self.__stock_service.get_etf_prices_daily( symbol.id, req.date_from_obj, req.date_to_obj) if temp: price_items.extend(temp) prices: DataFrame = self.__stock_service.get_price_dataframe( price_items) LogUtils.debug('Prices Init Shape={0}'.format(prices.shape)) return prices
def send(self, subject: str, body: str, content_type: str, to: List[str] = app_config.DFLT_EMAIL_TO, sender: str = app_config.GMAIL_USERNAME, pw: str = app_config.GMAIL_PW, server: str = app_config.GMAIL_SERVER, port: int = app_config.GMAIL_PORT) -> None: try: msg: MIMEMultipart = MIMEMultipart() msg['From'] = sender msg['To'] = ','.join(to) msg['Subject'] = '{0}{1}'.format(app_config.EMAIL_SUBJECT_PREFIX, subject) msg.attach(MIMEText(body, content_type)) LogUtils.debug(msg) with SMTP_SSL(server, port, context=ssl.create_default_context()) as server: server.ehlo() server.login(sender, pw) server.sendmail(sender, to, msg.as_string()) except Exception as ex: LogUtils.error('EMAIL ERROR!', ex)
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 close_position(self, symbol: str) -> Order: ret: Order = None try: LogUtils.debug('Close Position for = {0}'.format(symbol)) ret = self.__api.close_position(symbol) LogUtils.debug('Close Position response = {0}'.format(ret)) except Exception as ex: LogUtils.error('Close Position Error for {0}'.format(symbol), ex) raise ex return ret
import os from flask import Flask from flask_cors import CORS from flask_sqlalchemy import SQLAlchemy from apis.back_test_api import back_test_api from apis.import_api import import_api from apis.stock_api import stock_api from apis.trade_api import trade_api from app_consts import AppConsts from app_db import db from app_utils.log_utils import LogUtils from middlewares.error_handler import error_handler from middlewares.request_handler import request_handler LogUtils.debug('----- APP START -----') flask_env: str = os.environ.get('FLASK_ENV', AppConsts.FLASK_ENV_STG) LogUtils.debug('FLASK_ENV = {0}'.format(flask_env)) app: Flask = Flask(__name__, static_folder=AppConsts.STATIC_FOLDER) app.config.from_object('config_prod.ConfigProd' if flask_env == AppConsts.FLASK_ENV_PROD else 'config_stg.ConfigStg') app.register_blueprint(error_handler) app.register_blueprint(request_handler) app.register_blueprint(back_test_api, url_prefix=AppConsts.ROUTE_BACK_TEST) app.register_blueprint(stock_api, url_prefix=AppConsts.ROUTE_STOCK) app.register_blueprint(import_api, url_prefix=AppConsts.ROUTE_IMPORT) app.register_blueprint(trade_api, url_prefix=AppConsts.ROUTE_TRADE) CORS(app) db.init_app(app)
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 get_sample_prices_for_charts(self, req: CR) -> List[List[SPC]]: LogUtils.debug('START') ret: List[List[SPC]] = [] if not req or not req.is_valid_model(): raise BadRequestException() # region Init Symbols symbols: List[SymbolMaster] = self.get_symbols( AppConsts.INSTRUMENT_STOCK) if req.is_random_symbols: shuffle(symbols) symbols = symbols[:req.no_of_charts * 5] else: symbols = [s for s in symbols if s.symbol == req.symbol.upper()] if not symbols: return ret symbol_ids: List[int] = [symbol.id for symbol in symbols] # endregion LogUtils.debug('Symbols count={0}'.format(len(symbol_ids))) # region Init Prices prices = self.get_vw_symbol_spd_as_df( symbol_ids=symbol_ids, date_from=(req.date_from_obj - timedelta(days=300)), # for sma (will filter later) date_to=req.date_to_obj) # endregion LogUtils.debug('Prices Init Shape={0}'.format(prices.shape)) prices[AppConsts.CUSTOM_COL_PV] = prices[ AppConsts.PRICE_COL_CLOSE] * prices[AppConsts.PRICE_COL_VOLUME] prices[AppConsts.CUSTOM_COL_ADPV] = 0 for symbol in symbols: symbol_prices: DataFrame = prices.loc[[symbol.id]] if symbol_prices.empty: continue # region ADPV (50) adpvs: Series = symbol_prices[AppConsts.CUSTOM_COL_PV].rolling( window=50).mean() prices[AppConsts.CUSTOM_COL_ADPV].update(adpvs) adpv: float = adpvs.tail(1)[0] if adpv < AppConsts.ADPV_MIN_DFLT: continue symbol_prices = prices.loc[[symbol.id]] # endregion LogUtils.debug('START-Symbol={0},ADPV={1}'.format( symbol.symbol, adpv)) # SMA prices = self.__calc_service.append_sma( prices=prices, index=[symbol.id], sma_period=req.sma_period_1, sma_column_name=AppConsts.CUSTOM_COL_SMA_PERIOD_1) prices = self.__calc_service.append_sma( prices=prices, index=[symbol.id], sma_period=req.sma_period_2, sma_column_name=AppConsts.CUSTOM_COL_SMA_PERIOD_2) # Exponential Price Smoothing prices = self.__calc_service.append_exponential_smoothing_price( prices=prices, index=[symbol.id], alpha=req.exponential_smoothing_alpha) # Exponential Price Smoothing Max/Min prices = self.__calc_service.append_exponential_smoothing_max_min( prices=prices, index=[symbol.id], exponential_smoothing_max_min_diff=req. exponential_smoothing_max_min_diff) # Double Bottom prices = self.__calc_service.append_double_bottoms( prices=prices, index=[symbol.id], price_column=AppConsts.PRICE_COL_CLOSE, smooth_price_column=AppConsts. CUSTOM_COL_EXPONENTIAL_SMOOTHING_PRICE, max_column=AppConsts.CUSTOM_COL_EXPONENTIAL_SMOOTHING_MAX, min_column=AppConsts.CUSTOM_COL_EXPONENTIAL_SMOOTHING_MIN, double_bottoms_diff=5) # ABZ prices = self.__calc_service.append_abz( prices=prices, index=[symbol.id], abz_er_period=req.abz_er_period, abz_std_distance=req.abz_std_distance, abz_constant_k=req.abz_constant_k) symbol_prices = prices.loc[[symbol.id]] ret.append([ SPC(i, row) for i, row in symbol_prices[symbol_prices.index.get_level_values( AppConsts.PRICE_COL_DATE) >= req.date_from_obj].iterrows() ]) if len(ret) == req.no_of_charts: return ret LogUtils.debug('END') return ret
def handle_before_app_request() -> None: try: LogUtils.debug('START-Request={0}'.format(json.dumps(request.json, indent=2))) finally: pass
def handle_after_app_request(resp: Any) -> Any: try: LogUtils.debug('END-Request') finally: pass return resp
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