diff --git a/notebooks/profile.txt b/notebooks/profile.txt new file mode 100644 index 00000000..07d77e9b --- /dev/null +++ b/notebooks/profile.txt @@ -0,0 +1,56 @@ + 100360685 function calls (98707238 primitive calls) in 114.329 seconds + + Ordered by: cumulative time + List reduced from 4390 to 50 due to restriction <50> + + ncalls tottime percall cumtime percall filename:lineno(function) + 222/1 0.001 0.000 114.335 114.335 {built-in method builtins.exec} + 1072/1 0.002 0.000 114.108 114.108 decorator.py:229(fun) + 1 0.000 0.000 114.108 114.108 _common.py:7(finalize) + 1 0.025 0.025 109.966 109.966 strategy.py:325(screen) + 504 0.032 0.000 92.678 0.184 strategy.py:344(populate_pricing_data) + 504 0.006 0.000 70.988 0.141 _common.py:55(get_daily_candle_range) + 2020/504 0.039 0.000 70.763 0.140 utils.py:98(wrapper) + 504 0.241 0.000 70.717 0.140 base.py:84(history) + 504 0.009 0.000 61.634 0.122 data.py:332(get) + 508 0.003 0.000 61.501 0.121 sessions.py:593(get) + 508 0.006 0.000 61.498 0.121 sessions.py:502(request) + 509/508 0.011 0.000 60.400 0.119 sessions.py:673(send) + 4821 0.011 0.000 58.939 0.012 socket.py:692(readinto) + 4821 0.015 0.000 58.922 0.012 ssl.py:1299(recv_into) + 4821 0.007 0.000 58.906 0.012 ssl.py:1157(read) + 4821 58.899 0.012 58.899 0.012 {method 'read' of '_ssl._SSLSocket' objects} + 11180 0.009 0.000 53.715 0.005 {method 'readline' of '_io.BufferedReader' objects} + 509 0.008 0.000 52.808 0.104 adapters.py:434(send) + 509 0.013 0.000 52.604 0.103 connectionpool.py:534(urlopen) + 509 0.012 0.000 52.420 0.103 connectionpool.py:379(_make_request) + 509 0.003 0.000 51.809 0.102 client.py:1346(getresponse) + 509 0.009 0.000 51.795 0.102 client.py:318(begin) + 509 0.009 0.000 51.629 0.101 client.py:285(_read_status) + 504 0.013 0.000 18.724 0.037 fibStrategy.py:22(calculate_indicators) + 504 0.003 0.000 18.664 0.037 frame.py:9411(apply) + 504 0.059 0.000 18.656 0.037 apply.py:731(apply) + 502 0.002 0.000 18.594 0.037 apply.py:890(apply_standard) + 504 0.279 0.001 15.120 0.030 strategy.py:385(_screen_single_ticker) + 502 0.156 0.000 13.537 0.027 apply.py:896(apply_series_generator) + 297645 0.331 0.000 11.825 0.000 indexing.py:1059(__getitem__) + 294779 0.658 0.000 10.726 0.000 indexing.py:1592(_getitem_axis) + 78294 0.114 0.000 10.311 0.000 fibStrategy.py:23(apply_candle_body_outside_range) + 26516 0.027 0.000 10.178 0.000 strategy.py:409() + 53032 0.089 0.000 10.151 0.000 _base.py:36(evaluate_rules) +354465/351502 0.888 0.000 9.489 0.000 series.py:342(__init__) + 92751 0.324 0.000 7.958 0.000 frame.py:3703(_ixs) + 2529 0.003 0.000 7.558 0.003 models.py:887(content) + 5602 0.007 0.000 7.556 0.001 {method 'join' of 'bytes' objects} + 1926 0.003 0.000 7.549 0.004 models.py:812(generate) + 1926 0.002 0.000 7.546 0.004 response.py:607(stream) + 1913 0.012 0.000 7.543 0.004 response.py:789(read_chunked) + 14825 0.044 0.000 7.155 0.000 frame.py:609(__init__) + 3075 0.034 0.000 6.816 0.002 construction.py:423(dict_to_mgr) + 3520 0.011 0.000 5.613 0.002 construction.py:100(arrays_to_mgr) + 1914 0.004 0.000 5.247 0.003 response.py:767(_handle_chunk) + 2937 0.004 0.000 5.243 0.002 client.py:631(_safe_read) + 3630 0.008 0.000 5.241 0.001 {method 'read' of '_io.BufferedReader' objects} + 53032 0.112 0.000 5.142 0.000 _base.py:51(_evaluate_bullish_rules) + 502 0.003 0.000 5.056 0.010 apply.py:915(wrap_results) + 502 0.001 0.000 5.046 0.010 apply.py:1050(wrap_results_for_axis) \ No newline at end of file diff --git a/stockMarket/technicalAnalysis/strategy.py b/stockMarket/technicalAnalysis/strategy.py index 11ce9d73..dac46a0e 100644 --- a/stockMarket/technicalAnalysis/strategy.py +++ b/stockMarket/technicalAnalysis/strategy.py @@ -40,6 +40,44 @@ def __init__(self, init_from_json: bool = False, **kwargs ) -> None: + """ + The strategy class is used to screen a list of tickers for trades based on a list of strategy objects. The strategy objects are used to evaluate the rules for each ticker and the trades are executed based on the outcome of the rules. The trades are stored in a dictionary with the ticker as the key and a list of trades as the value. The trades are then written to an xlsx file. + + In general, the strategy class can be initialized in two ways: + 1. By providing all the necessary parameters to the __init__ method. + 2. By providing the dir_path to the directory where the json files are stored. The json files are used to initialize the strategy class. (This setting is useful when the screening was already performed and the trades are to be analyzed or the screening is to be continued.) + + Parameters + ---------- + strategy_objects : List[StrategyObject] + A list of strategy objects that are used to evaluate the rules for each ticker. + start_date : str + The start date for the screening in the format "dd.mm.yyyy". + end_date : str + The end date for the screening in the format "dd.mm.yyyy". + rule_enums : List[RuleEnum], optional + A list of rule enums that are used to evaluate the rules for each ticker, by default [RuleEnum.BULLISH] + num_batches : int, optional + The number of batches to split the screening into, by default 1 + batch_size : Optional[pd.Timedelta], optional + The size of each batch, by default None + If None, the batch size is calculated based on the number of batches and the start and end date. If both num_batches and batch_size are provided, num_batches will be ignored. + trade_settings : Optional[TradeSettings], optional + The trade settings that are used to execute the trades, by default None + candle_period : str | Period | None, optional + The candle period for the pricing data, by default None (daily candle period is used) + use_earnings_dates : bool, optional + A boolean indicating whether the earnings dates should be used to filter the trades, by default False + finalize_commands : Optional[List[str]], optional + A list of shell commands that are executed after the screening is finished, by default None + init_from_json : bool, optional + A boolean indicating whether the strategy class should be initialized from a json file, by default False + + Raises + ------ + ValueError + init_from_json is True and neither dir_path nor the combination of dir_name and base_path is provided in kwargs. + """ if not init_from_json: self.__clean_init__( @@ -79,6 +117,33 @@ def __clean_init__(self, finalize_commands: Optional[List[str]] = None, **kwargs ) -> None: + """ + Initialize the strategy class by providing all the necessary parameters. + + Parameters + ---------- + strategy_objects : List[StrategyObject] + A list of strategy objects that are used to evaluate the rules for each ticker. + start_date : str + The start date for the screening in the format "dd.mm.yyyy". + end_date : str + The end date for the screening in the format "dd.mm.yyyy". + rule_enums : List[RuleEnum], optional + A list of rule enums that are used to evaluate the rules for each ticker, by default [RuleEnum.BULLISH] + num_batches : int, optional + The number of batches to split the screening into, by default 1 + batch_size : Optional[pd.Timedelta], optional + The size of each batch, by default None + If None, the batch size is calculated based on the number of batches and the start and end date. If both num_batches and batch_size are provided, num_batches will be ignored. + trade_settings : Optional[TradeSettings], optional + The trade settings that are used to execute the trades, by default None + candle_period : str | Period | None, optional + The candle period for the pricing data, by default None (daily candle period is used) + use_earnings_dates : bool, optional + A boolean indicating whether the earnings dates should be used to filter the trades, by default False + finalize_commands : Optional[List[str]], optional + A list of shell commands that are executed after the screening is finished, by default None + """ self.strategy_objects = strategy_objects self.rule_enums = rule_enums @@ -89,6 +154,8 @@ def __clean_init__(self, self.earnings_calendar: Dict[str, List[dt.date]] = {} self.file_settings = None + # setup files including creating the directory + # all input parameters used for the setup have to be given as kwargs self.setup_files( self.strategy_objects, self.rule_enums, @@ -128,7 +195,15 @@ def __clean_init__(self, dir_path=self.dir_path, ) - def init_from_json(self, dir_path: Path): + def __init_from_json__(self, dir_path: Path): + """ + Initialize the strategy class from a json file. + + Parameters + ---------- + dir_path : Path + The directory path where the json file(s) is/are stored. + """ StrategyJSON.read(dir_path) self.file_settings = StrategyJSON.file_settings @@ -180,6 +255,26 @@ def setup_dates(self, num_batches: int, batch_size: Optional[pd.Timedelta] = None, ): + """ + Setup the dates for the screening. + + Parameters + ---------- + start_date : str + start date in the format "dd.mm.yyyy" + end_date : str + end date in the format "dd.mm.yyyy" + candle_period : str | Period | None + candle period for the pricing data, by default None (daily candle period is used) + num_batches : int + number of batches to split the screening into + batch_size : Optional[pd.Timedelta], optional + size of each batch, by default None (calculated based on the number of batches and the start and end date). If both num_batches and batch_size are provided, num_batches will be ignored. + + Warnings + -------- + If both num_batches and batch_size are set, num_batches will be ignored. + """ if candle_period is None: self.candle_period = Period('daily') else: @@ -198,6 +293,14 @@ def setup_dates(self, "Both num_batches and batch_size are set. num_batches will be ignored") def setup_trade_settings(self, trade_settings: Optional[TradeSettings] = None): + """ + Setup the trade settings for the trades. + + Parameters + ---------- + trade_settings : Optional[TradeSettings], optional + trade settings for the trades, by default None + """ self.trade_settings = trade_settings if trade_settings is not None else TradeSettings() def get_earnings_dates(self): diff --git a/stockMarket/technicalAnalysis/trade/common.py b/stockMarket/technicalAnalysis/trade/common.py index c1039793..a7b25434 100644 --- a/stockMarket/technicalAnalysis/trade/common.py +++ b/stockMarket/technicalAnalysis/trade/common.py @@ -2,6 +2,7 @@ import numpy as np from stockMarket.yfinance._common import get_daily_candle_range +from .enums import TradeStatus, TradeOutcome def calc_highest_body_price(candle): @@ -12,52 +13,6 @@ def calc_lowest_body_price(candle): return min(candle.open, candle.close) -def find_daily_candle(ticker, - pricing: pd.DataFrame, - candle_index: int, - target_price: float, - mode=np.greater_equal, - min_date=None, - ): - - start_date = pricing.index[candle_index].date() - if candle_index == len(pricing)-1: - end_date = None - else: - # end date can be set to the next candle date for all intervals - # e.g if interval is weekly than end date is the next week but - # yf will not include the first day of the next week - end_date = pricing.index[candle_index + 1].date() - end_date = str(end_date) - - min_date = min_date if min_date is not None and min_date > start_date else start_date - - if end_date is not None and min_date == end_date: - return None, None - - pricing_daily = get_daily_candle_range(ticker, min_date, end_date) - - candle = None - - for i in range(len(pricing_daily)): - candle = pricing_daily.iloc[i] - - check_price = candle.high if mode == np.greater_equal else candle.low - - if mode(candle.open, target_price): - target_price = candle.open - break - elif mode(check_price, target_price): - break - - candle = None - - if candle is None: - return None, None - else: - return candle, target_price - - def find_last_high( pricing: pd.DataFrame, ref_candle_index: int, diff --git a/stockMarket/technicalAnalysis/trade/enums_api.py b/stockMarket/technicalAnalysis/trade/enums_api.py new file mode 100644 index 00000000..938b7577 --- /dev/null +++ b/stockMarket/technicalAnalysis/trade/enums_api.py @@ -0,0 +1,32 @@ +from beartype.typing import Optional + +from .enums import TradeOutcome, TradeStatus + + +def check_PL_ratio( + PL: float, + min_PL: Optional[float] = None, + max_PL: Optional[float] = None, +): + if min_PL is not None and PL < min_PL: + return TradeStatus.PL_TOO_SMALL + + if max_PL is not None and PL > max_PL: + return TradeStatus.PL_TOO_LARGE + + return TradeStatus.UNKNOWN + + +def determine_outcome_status( + trade_status: TradeStatus, + ENTRY: float, + EXIT: float, +) -> TradeOutcome: + + if trade_status == TradeStatus.CLOSED: + if EXIT > ENTRY: + return TradeOutcome.WIN + + return TradeOutcome.LOSS + + return TradeOutcome.NONE diff --git a/stockMarket/technicalAnalysis/trade/trade.py b/stockMarket/technicalAnalysis/trade/trade.py index 1487a96b..8b1149e4 100644 --- a/stockMarket/technicalAnalysis/trade/trade.py +++ b/stockMarket/technicalAnalysis/trade/trade.py @@ -25,6 +25,8 @@ from beartype.typing import Optional from .enums import ChartEnum, TradeStatus, TradeOutcome, AttachedOrderType +from .enums_api import determine_outcome_status, check_PL_ratio + from .tradeSettings import TradeSettings from .decorators import ignore_trade_exceptions, check_trade_status from .common import ( @@ -38,7 +40,7 @@ class Trade: def __init__(self, ticker: str, trigger_candle: pd.DataFrame, - settings=None, + settings: Optional[TradeSettings] = None, ) -> None: self.ticker = ticker @@ -52,8 +54,6 @@ def __init__(self, self.EXIT: Optional[float] = None self.TP: Optional[float] = None - self.condition = None - self.trade_status = TradeStatus.UNKNOWN self.settings = settings if settings is not None else TradeSettings() @@ -89,25 +89,19 @@ def execute_trade(self, self.calc_EXIT(pricing_daily=pricing_daily) - self.determine_trade_outcome() - - def determine_trade_outcome(self): - if self.trade_status == TradeStatus.CLOSED: - if self.EXIT > self.R_ENTRY: - self.outcome_status = TradeOutcome.WIN - elif self.EXIT < self.R_ENTRY: - self.outcome_status = TradeOutcome.LOSS - else: - self.outcome_status = TradeOutcome.NONE + self.outcome_status = determine_outcome_status( + self.trade_status, + self.R_ENTRY, + self.EXIT + ) @check_trade_status def check_PL_RATIOS(self): - - if self.settings.min_PL is not None and self.PL < self.settings.min_PL: - self.trade_status = TradeStatus.PL_TOO_SMALL - - if self.settings.max_PL is not None and self.PL > self.settings.max_PL: - self.trade_status = TradeStatus.PL_TOO_LARGE + self.trade_status = check_PL_ratio( + PL=self.PL, + min_PL=self.settings.min_PL, + max_PL=self.settings.max_PL + ) @check_trade_status def setup_TP(self,