pkscreener 0.46.20250909.768__cp312-cp312-win_amd64.whl → 0.46.20250911.771__cp312-cp312-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pkscreener-0.46.20250911.771.data/purelib/pkscreener/MainApplication.py +721 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/README.txt +5 -5
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/AssetsManager.py +12 -12
- pkscreener-0.46.20250911.771.data/purelib/pkscreener/classes/MenuManager.py +2373 -0
- pkscreener-0.46.20250911.771.data/purelib/pkscreener/classes/PKScreenerMain.py +916 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/ScreeningStatistics.py +2 -0
- pkscreener-0.46.20250911.771.data/purelib/pkscreener/classes/__init__.py +1 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/pkscreenercli.py +1 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/requirements.txt +1 -0
- {pkscreener-0.46.20250909.768.dist-info → pkscreener-0.46.20250911.771.dist-info}/METADATA +7 -7
- pkscreener-0.46.20250911.771.dist-info/RECORD +61 -0
- pkscreener-0.46.20250909.768.data/purelib/pkscreener/classes/__init__.py +0 -1
- pkscreener-0.46.20250909.768.dist-info/RECORD +0 -58
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/Disclaimer.txt +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/LICENSE-Others.txt +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/LICENSE.txt +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/LogoWM.txt +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/__init__.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/ArtTexts.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/Backtest.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/Barometer.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/BaseScreeningStatistics.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/CandlePatterns.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/Changelog.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/ConfigManager.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/ConsoleMenuUtility.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/ConsoleUtility.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/Fetcher.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/GlobalStore.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/ImageUtility.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/MarketMonitor.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/MarketStatus.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/MenuOptions.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/Messenger.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/OtaUpdater.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKAnalytics.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKDataService.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKDemoHandler.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKMarketOpenCloseAnalyser.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKPremiumHandler.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKScanRunner.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKScheduledTaskProgress.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKScheduler.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKSpreadsheets.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKTask.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKUserRegistration.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/Pktalib.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/Portfolio.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PortfolioXRay.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/StockScreener.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/StockSentiment.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/UserMenuChoicesHandler.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/Utility.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/WorkflowManager.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/keys.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/courbd.ttf +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/globals.py +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/pkscreener.ini +0 -0
- {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/pkscreenerbot.py +0 -0
- {pkscreener-0.46.20250909.768.dist-info → pkscreener-0.46.20250911.771.dist-info}/WHEEL +0 -0
- {pkscreener-0.46.20250909.768.dist-info → pkscreener-0.46.20250911.771.dist-info}/entry_points.txt +0 -0
- {pkscreener-0.46.20250909.768.dist-info → pkscreener-0.46.20250911.771.dist-info}/licenses/LICENSE +0 -0
- {pkscreener-0.46.20250909.768.dist-info → pkscreener-0.46.20250911.771.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,2373 @@
|
|
1
|
+
#!/usr/bin/python3
|
2
|
+
"""
|
3
|
+
The MIT License (MIT)
|
4
|
+
|
5
|
+
Copyright (c) 2023 pkjmesra
|
6
|
+
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
9
|
+
in the Software without restriction, including without limitation the rights
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
12
|
+
furnished to do so, subject to the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
15
|
+
copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
23
|
+
SOFTWARE.
|
24
|
+
|
25
|
+
"""
|
26
|
+
|
27
|
+
import os
|
28
|
+
import random
|
29
|
+
import warnings
|
30
|
+
warnings.simplefilter("ignore", UserWarning, append=True)
|
31
|
+
os.environ["PYTHONWARNINGS"] = "ignore::UserWarning"
|
32
|
+
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
|
33
|
+
import logging
|
34
|
+
import multiprocessing
|
35
|
+
import sys
|
36
|
+
import time
|
37
|
+
import urllib
|
38
|
+
import warnings
|
39
|
+
from datetime import datetime, UTC, timedelta
|
40
|
+
from time import sleep
|
41
|
+
|
42
|
+
import numpy as np
|
43
|
+
|
44
|
+
warnings.simplefilter("ignore", DeprecationWarning)
|
45
|
+
warnings.simplefilter("ignore", FutureWarning)
|
46
|
+
import pandas as pd
|
47
|
+
from alive_progress import alive_bar
|
48
|
+
from PKDevTools.classes.Committer import Committer
|
49
|
+
from PKDevTools.classes.ColorText import colorText
|
50
|
+
from PKDevTools.classes.PKDateUtilities import PKDateUtilities
|
51
|
+
from PKDevTools.classes.log import default_logger
|
52
|
+
from PKDevTools.classes.SuppressOutput import SuppressOutput
|
53
|
+
from PKDevTools.classes import Archiver
|
54
|
+
from PKDevTools.classes.Telegram import (
|
55
|
+
is_token_telegram_configured,
|
56
|
+
send_document,
|
57
|
+
send_message,
|
58
|
+
send_photo,
|
59
|
+
send_media_group
|
60
|
+
)
|
61
|
+
from PKNSETools.morningstartools.PKMorningstarDataFetcher import morningstarDataFetcher
|
62
|
+
from PKNSETools.Nasdaq.PKNasdaqIndex import PKNasdaqIndexFetcher
|
63
|
+
from tabulate import tabulate
|
64
|
+
from halo import Halo
|
65
|
+
|
66
|
+
import pkscreener.classes.ConfigManager as ConfigManager
|
67
|
+
import pkscreener.classes.Fetcher as Fetcher
|
68
|
+
import pkscreener.classes.ScreeningStatistics as ScreeningStatistics
|
69
|
+
from pkscreener.classes import Utility, ConsoleUtility, ConsoleMenuUtility, ImageUtility
|
70
|
+
from pkscreener.classes.Utility import STD_ENCODING
|
71
|
+
from pkscreener.classes import VERSION, PortfolioXRay
|
72
|
+
from pkscreener.classes.Backtest import backtest, backtestSummary
|
73
|
+
from pkscreener.classes.PKSpreadsheets import PKSpreadsheets
|
74
|
+
from PKDevTools.classes.OutputControls import OutputControls
|
75
|
+
from PKDevTools.classes.Environment import PKEnvironment
|
76
|
+
from pkscreener.classes.CandlePatterns import CandlePatterns
|
77
|
+
from pkscreener.classes import AssetsManager
|
78
|
+
from PKDevTools.classes.FunctionTimeouts import exit_after
|
79
|
+
from pkscreener.classes.MenuOptions import (
|
80
|
+
level0MenuDict,
|
81
|
+
level1_X_MenuDict,
|
82
|
+
level1_P_MenuDict,
|
83
|
+
level2_X_MenuDict,
|
84
|
+
level2_P_MenuDict,
|
85
|
+
level3_X_ChartPattern_MenuDict,
|
86
|
+
level3_X_PopularStocks_MenuDict,
|
87
|
+
level3_X_PotentialProfitable_MenuDict,
|
88
|
+
PRICE_CROSS_SMA_EMA_DIRECTION_MENUDICT,
|
89
|
+
PRICE_CROSS_SMA_EMA_TYPE_MENUDICT,
|
90
|
+
PRICE_CROSS_PIVOT_POINT_TYPE_MENUDICT,
|
91
|
+
level3_X_Reversal_MenuDict,
|
92
|
+
level4_X_Lorenzian_MenuDict,
|
93
|
+
level4_X_ChartPattern_Confluence_MenuDict,
|
94
|
+
level4_X_ChartPattern_BBands_SQZ_MenuDict,
|
95
|
+
level4_X_ChartPattern_MASignalMenuDict,
|
96
|
+
level1_index_options_sectoral,
|
97
|
+
menus,
|
98
|
+
MAX_SUPPORTED_MENU_OPTION,
|
99
|
+
MAX_MENU_OPTION,
|
100
|
+
PIPED_SCANNERS,
|
101
|
+
PREDEFINED_SCAN_MENU_KEYS,
|
102
|
+
PREDEFINED_SCAN_MENU_TEXTS,
|
103
|
+
INDICES_MAP,
|
104
|
+
CANDLESTICK_DICT
|
105
|
+
)
|
106
|
+
from pkscreener.classes.OtaUpdater import OTAUpdater
|
107
|
+
from pkscreener.classes.Portfolio import PortfolioCollection
|
108
|
+
from pkscreener.classes.PKTask import PKTask
|
109
|
+
from pkscreener.classes.PKScheduler import PKScheduler
|
110
|
+
from pkscreener.classes.PKScanRunner import PKScanRunner
|
111
|
+
from pkscreener.classes.PKMarketOpenCloseAnalyser import PKMarketOpenCloseAnalyser
|
112
|
+
from pkscreener.classes.PKPremiumHandler import PKPremiumHandler
|
113
|
+
from pkscreener.classes.AssetsManager import PKAssetsManager
|
114
|
+
from pkscreener.classes.PKAnalytics import PKAnalyticsService
|
115
|
+
|
116
|
+
if __name__ == '__main__':
|
117
|
+
multiprocessing.freeze_support()
|
118
|
+
|
119
|
+
# Constants
|
120
|
+
np.seterr(divide="ignore", invalid="ignore")
|
121
|
+
TEST_STKCODE = "SBIN"
|
122
|
+
|
123
|
+
|
124
|
+
class MenuManager:
|
125
|
+
"""
|
126
|
+
Manages all menu navigation, selection, and hierarchy for the PKScreener application.
|
127
|
+
Handles user input, menu rendering, and option validation across all menu levels.
|
128
|
+
"""
|
129
|
+
|
130
|
+
def __init__(self, config_manager, user_passed_args):
|
131
|
+
"""
|
132
|
+
Initialize the MenuManager with configuration and user arguments.
|
133
|
+
|
134
|
+
Args:
|
135
|
+
config_manager: Configuration manager instance
|
136
|
+
user_passed_args: User passed arguments
|
137
|
+
"""
|
138
|
+
self.config_manager = config_manager
|
139
|
+
self.user_passed_args = user_passed_args
|
140
|
+
self.m0 = menus()
|
141
|
+
self.m1 = menus()
|
142
|
+
self.m2 = menus()
|
143
|
+
self.m3 = menus()
|
144
|
+
self.m4 = menus()
|
145
|
+
self.selected_choice = {"0": "", "1": "", "2": "", "3": "", "4": ""}
|
146
|
+
self.menu_choice_hierarchy = ""
|
147
|
+
self.n_value_for_menu = 0
|
148
|
+
|
149
|
+
def ensure_menus_loaded(self, menu_option=None, index_option=None, execute_option=None):
|
150
|
+
"""
|
151
|
+
Ensure all menus are loaded and rendered for the given options.
|
152
|
+
|
153
|
+
Args:
|
154
|
+
menu_option: Selected menu option
|
155
|
+
index_option: Selected index option
|
156
|
+
execute_option: Selected execute option
|
157
|
+
"""
|
158
|
+
try:
|
159
|
+
if len(self.m0.menuDict.keys()) == 0:
|
160
|
+
self.m0.renderForMenu(asList=True)
|
161
|
+
|
162
|
+
if len(self.m1.menuDict.keys()) == 0:
|
163
|
+
self.m1.renderForMenu(selected_menu=self.m0.find(menu_option), asList=True)
|
164
|
+
|
165
|
+
if len(self.m2.menuDict.keys()) == 0:
|
166
|
+
self.m2.renderForMenu(selected_menu=self.m1.find(index_option), asList=True)
|
167
|
+
|
168
|
+
if len(self.m3.menuDict.keys()) == 0:
|
169
|
+
self.m3.renderForMenu(selected_menu=self.m2.find(execute_option), asList=True)
|
170
|
+
except:
|
171
|
+
pass
|
172
|
+
|
173
|
+
def init_execution(self, menu_option=None):
|
174
|
+
"""
|
175
|
+
Initialize execution by showing main menu and getting user selection.
|
176
|
+
|
177
|
+
Args:
|
178
|
+
menu_option: Pre-selected menu option
|
179
|
+
|
180
|
+
Returns:
|
181
|
+
object: Selected menu object
|
182
|
+
"""
|
183
|
+
ConsoleUtility.PKConsoleTools.clearScreen(forceTop=True)
|
184
|
+
|
185
|
+
if self.user_passed_args is not None and self.user_passed_args.pipedmenus is not None:
|
186
|
+
OutputControls().printOutput(
|
187
|
+
colorText.FAIL
|
188
|
+
+ " [+] You chose: "
|
189
|
+
+ f" (Piped Scan Mode) [{self.user_passed_args.pipedmenus}]"
|
190
|
+
+ colorText.END
|
191
|
+
)
|
192
|
+
|
193
|
+
self.m0.renderForMenu(selected_menu=None, asList=(self.user_passed_args is not None and self.user_passed_args.options is not None))
|
194
|
+
|
195
|
+
try:
|
196
|
+
needs_calc = self.user_passed_args is not None and self.user_passed_args.backtestdaysago is not None
|
197
|
+
past_date = f" [+] [ Running in Quick Backtest Mode for {colorText.WARN}{PKDateUtilities.nthPastTradingDateStringFromFutureDate(int(self.user_passed_args.backtestdaysago) if needs_calc else 0)}{colorText.END} ]\n" if needs_calc else ""
|
198
|
+
|
199
|
+
if menu_option is None:
|
200
|
+
if "PKDevTools_Default_Log_Level" in os.environ.keys():
|
201
|
+
from PKDevTools.classes import Archiver
|
202
|
+
log_file_path = os.path.join(Archiver.get_user_data_dir(), "pkscreener-logs.txt")
|
203
|
+
OutputControls().printOutput(colorText.FAIL + "\n [+] Logs will be written to:" + colorText.END)
|
204
|
+
OutputControls().printOutput(colorText.GREEN + f" [+] {log_file_path}" + colorText.END)
|
205
|
+
OutputControls().printOutput(colorText.FAIL + " [+] If you need to share,run through the menus that are causing problems. At the end, open this folder, zip the log file to share at https://github.com/pkjmesra/PKScreener/issues .\n" + colorText.END)
|
206
|
+
|
207
|
+
menu_option = input(colorText.FAIL + f"{past_date} [+] Select option: ") or "P"
|
208
|
+
OutputControls().printOutput(colorText.END, end="")
|
209
|
+
|
210
|
+
if menu_option == "" or menu_option is None:
|
211
|
+
menu_option = "X"
|
212
|
+
|
213
|
+
menu_option = menu_option.upper()
|
214
|
+
selected_menu = self.m0.find(menu_option)
|
215
|
+
|
216
|
+
if selected_menu is not None:
|
217
|
+
if selected_menu.menuKey == "Z":
|
218
|
+
OutputControls().takeUserInput(
|
219
|
+
colorText.FAIL
|
220
|
+
+ " [+] Press <Enter> to Exit!"
|
221
|
+
+ colorText.END
|
222
|
+
)
|
223
|
+
PKAnalyticsService().send_event("app_exit")
|
224
|
+
sys.exit(0)
|
225
|
+
elif selected_menu.menuKey in ["B", "C", "G", "H", "U", "T", "S", "E", "X", "Y", "M", "D", "I", "L", "F"]:
|
226
|
+
ConsoleUtility.PKConsoleTools.clearScreen(forceTop=True)
|
227
|
+
self.selected_choice["0"] = selected_menu.menuKey
|
228
|
+
return selected_menu
|
229
|
+
elif selected_menu.menuKey in ["P"]:
|
230
|
+
return selected_menu
|
231
|
+
|
232
|
+
except KeyboardInterrupt:
|
233
|
+
raise KeyboardInterrupt
|
234
|
+
except Exception as e:
|
235
|
+
default_logger().debug(e, exc_info=True)
|
236
|
+
self.show_option_error_message()
|
237
|
+
return self.init_execution()
|
238
|
+
|
239
|
+
self.show_option_error_message()
|
240
|
+
return self.init_execution()
|
241
|
+
|
242
|
+
def init_post_level0_execution(self, menu_option=None, index_option=None, execute_option=None, skip=[], retrial=False):
|
243
|
+
"""
|
244
|
+
Initialize execution after level 0 menu selection.
|
245
|
+
|
246
|
+
Args:
|
247
|
+
menu_option: Selected menu option
|
248
|
+
index_option: Selected index option
|
249
|
+
execute_option: Selected execute option
|
250
|
+
skip: List of options to skip
|
251
|
+
retrial (bool): Whether this is a retry
|
252
|
+
|
253
|
+
Returns:
|
254
|
+
tuple: Index option and execute option
|
255
|
+
"""
|
256
|
+
ConsoleUtility.PKConsoleTools.clearScreen(forceTop=True)
|
257
|
+
|
258
|
+
if menu_option is None:
|
259
|
+
OutputControls().printOutput('You must choose an option from the previous menu! Defaulting to "X"...')
|
260
|
+
menu_option = "X"
|
261
|
+
|
262
|
+
OutputControls().printOutput(
|
263
|
+
colorText.FAIL
|
264
|
+
+ " [+] You chose: "
|
265
|
+
+ level0MenuDict[menu_option].strip()
|
266
|
+
+ (f" (Piped Scan Mode) [{self.user_passed_args.pipedmenus}]" if (self.user_passed_args is not None and self.user_passed_args.pipedmenus is not None) else "")
|
267
|
+
+ colorText.END
|
268
|
+
)
|
269
|
+
|
270
|
+
if index_option is None:
|
271
|
+
selected_menu = self.m0.find(menu_option)
|
272
|
+
self.m1.renderForMenu(selected_menu=selected_menu, skip=skip, asList=(self.user_passed_args is not None and self.user_passed_args.options is not None))
|
273
|
+
|
274
|
+
try:
|
275
|
+
needs_calc = self.user_passed_args is not None and self.user_passed_args.backtestdaysago is not None
|
276
|
+
past_date = f" [+] [ Running in Quick Backtest Mode for {colorText.WARN}{PKDateUtilities.nthPastTradingDateStringFromFutureDate(int(self.user_passed_args.backtestdaysago) if needs_calc else 0)}{colorText.END} ]\n" if needs_calc else ""
|
277
|
+
|
278
|
+
if index_option is None:
|
279
|
+
index_option = OutputControls().takeUserInput(
|
280
|
+
colorText.FAIL + f"{past_date} [+] Select option: "
|
281
|
+
)
|
282
|
+
OutputControls().printOutput(colorText.END, end="")
|
283
|
+
|
284
|
+
if (str(index_option).isnumeric() and int(index_option) > 1 and str(execute_option).isnumeric() and int(str(execute_option)) <= MAX_SUPPORTED_MENU_OPTION) or \
|
285
|
+
str(index_option).upper() in ["S", "E", "W"]:
|
286
|
+
self.ensure_menus_loaded(menu_option, index_option, execute_option)
|
287
|
+
|
288
|
+
if not PKPremiumHandler.hasPremium(self.m1.find(str(index_option).upper())):
|
289
|
+
PKAnalyticsService().send_event(f"non_premium_user_{menu_option}_{index_option}_{execute_option}")
|
290
|
+
return None, None
|
291
|
+
|
292
|
+
if index_option == "" or index_option is None:
|
293
|
+
index_option = int(self.config_manager.defaultIndex)
|
294
|
+
elif not str(index_option).isnumeric():
|
295
|
+
index_option = index_option.upper()
|
296
|
+
|
297
|
+
if index_option in ["M", "E", "N", "Z"]:
|
298
|
+
return index_option, 0
|
299
|
+
else:
|
300
|
+
index_option = int(index_option)
|
301
|
+
|
302
|
+
if index_option < 0 or index_option > 15:
|
303
|
+
raise ValueError
|
304
|
+
elif index_option == 13:
|
305
|
+
self.config_manager.period = "2y"
|
306
|
+
self.config_manager.getConfig(ConfigManager.parser)
|
307
|
+
self.newlyListedOnly = True
|
308
|
+
index_option = 12
|
309
|
+
|
310
|
+
if index_option == 15:
|
311
|
+
from pkscreener.classes.MarketStatus import MarketStatus
|
312
|
+
MarketStatus().exchange = "^IXIC"
|
313
|
+
|
314
|
+
self.selected_choice["1"] = str(index_option)
|
315
|
+
except KeyboardInterrupt:
|
316
|
+
raise KeyboardInterrupt
|
317
|
+
except Exception as e:
|
318
|
+
default_logger().debug(e, exc_info=True)
|
319
|
+
OutputControls().printOutput(
|
320
|
+
colorText.FAIL
|
321
|
+
+ "\n [+] Please enter a valid numeric option & Try Again!"
|
322
|
+
+ colorText.END
|
323
|
+
)
|
324
|
+
|
325
|
+
if not retrial:
|
326
|
+
sleep(2)
|
327
|
+
ConsoleUtility.PKConsoleTools.clearScreen(forceTop=True)
|
328
|
+
return self.init_post_level0_execution(retrial=True)
|
329
|
+
|
330
|
+
return index_option, execute_option
|
331
|
+
|
332
|
+
def init_post_level1_execution(self, index_option, execute_option=None, skip=[], retrial=False):
|
333
|
+
"""
|
334
|
+
Initialize execution after level 1 menu selection.
|
335
|
+
|
336
|
+
Args:
|
337
|
+
index_option: Selected index option
|
338
|
+
execute_option: Selected execute option
|
339
|
+
skip: List of options to skip
|
340
|
+
retrial (bool): Whether this is a retry
|
341
|
+
|
342
|
+
Returns:
|
343
|
+
tuple: Index option and execute option
|
344
|
+
"""
|
345
|
+
list_stock_codes = [] if self.list_stock_codes is None or len(self.list_stock_codes) == 0 else self.list_stock_codes
|
346
|
+
|
347
|
+
if execute_option is None:
|
348
|
+
if index_option is not None and index_option != "W":
|
349
|
+
ConsoleUtility.PKConsoleTools.clearScreen(forceTop=True)
|
350
|
+
OutputControls().printOutput(
|
351
|
+
colorText.FAIL
|
352
|
+
+ " [+] You chose: "
|
353
|
+
+ level0MenuDict[self.selected_choice["0"]].strip()
|
354
|
+
+ " > "
|
355
|
+
+ level1_X_MenuDict[self.selected_choice["1"]].strip()
|
356
|
+
+ (f" (Piped Scan Mode) [{self.user_passed_args.pipedmenus}]" if (self.user_passed_args is not None and self.user_passed_args.pipedmenus is not None) else "")
|
357
|
+
+ colorText.END
|
358
|
+
)
|
359
|
+
|
360
|
+
selected_menu = self.m1.find(index_option)
|
361
|
+
self.m2.renderForMenu(selected_menu=selected_menu, skip=skip, asList=(self.user_passed_args is not None and self.user_passed_args.options is not None))
|
362
|
+
stock_index_code = str(len(level1_index_options_sectoral.keys()))
|
363
|
+
|
364
|
+
if index_option == "S":
|
365
|
+
self.ensure_menus_loaded("X", index_option, execute_option)
|
366
|
+
|
367
|
+
if not PKPremiumHandler.hasPremium(selected_menu):
|
368
|
+
PKAnalyticsService().send_event(f"non_premium_user_X_{index_option}_{execute_option}")
|
369
|
+
PKAnalyticsService().send_event("app_exit")
|
370
|
+
sys.exit(0)
|
371
|
+
|
372
|
+
index_keys = level1_index_options_sectoral.keys()
|
373
|
+
stock_index_code = input(
|
374
|
+
colorText.FAIL + " [+] Select option: "
|
375
|
+
) or str(len(index_keys))
|
376
|
+
OutputControls().printOutput(colorText.END, end="")
|
377
|
+
|
378
|
+
if stock_index_code == str(len(index_keys)):
|
379
|
+
for index_code in index_keys:
|
380
|
+
if index_code != str(len(index_keys)):
|
381
|
+
self.list_stock_codes.append(level1_index_options_sectoral[str(index_code)].split("(")[1].split(")")[0])
|
382
|
+
else:
|
383
|
+
self.list_stock_codes = [level1_index_options_sectoral[str(stock_index_code)].split("(")[1].split(")")[0]]
|
384
|
+
|
385
|
+
selected_menu.menuKey = "0" # Reset because user must have selected specific index menu with single stock
|
386
|
+
ConsoleUtility.PKConsoleTools.clearScreen(forceTop=True)
|
387
|
+
self.m2.renderForMenu(selected_menu=selected_menu, skip=skip, asList=(self.user_passed_args is not None and self.user_passed_args.options is not None))
|
388
|
+
|
389
|
+
try:
|
390
|
+
needs_calc = self.user_passed_args is not None and self.user_passed_args.backtestdaysago is not None
|
391
|
+
past_date = f" [+] [ Running in Quick Backtest Mode for {colorText.WARN}{PKDateUtilities.nthPastTradingDateStringFromFutureDate(int(self.user_passed_args.backtestdaysago) if needs_calc else 0)}{colorText.END} ]\n" if needs_calc else ""
|
392
|
+
|
393
|
+
if index_option is not None and index_option != "W":
|
394
|
+
if execute_option is None:
|
395
|
+
execute_option = input(
|
396
|
+
colorText.FAIL + f"{past_date} [+] Select option: "
|
397
|
+
) or "9"
|
398
|
+
OutputControls().printOutput(colorText.END, end="")
|
399
|
+
|
400
|
+
self.ensure_menus_loaded("X", index_option, execute_option)
|
401
|
+
|
402
|
+
if not PKPremiumHandler.hasPremium(self.m2.find(str(execute_option))):
|
403
|
+
PKAnalyticsService().send_event(f"non_premium_user_X_{index_option}_{execute_option}")
|
404
|
+
return None, None
|
405
|
+
|
406
|
+
if execute_option == "":
|
407
|
+
execute_option = 1
|
408
|
+
|
409
|
+
if not str(execute_option).isnumeric():
|
410
|
+
execute_option = execute_option.upper()
|
411
|
+
else:
|
412
|
+
execute_option = int(execute_option)
|
413
|
+
|
414
|
+
if execute_option < 0 or execute_option > MAX_MENU_OPTION:
|
415
|
+
raise ValueError
|
416
|
+
else:
|
417
|
+
execute_option = 0
|
418
|
+
|
419
|
+
self.selected_choice["2"] = str(execute_option)
|
420
|
+
except KeyboardInterrupt:
|
421
|
+
raise KeyboardInterrupt
|
422
|
+
except Exception as e:
|
423
|
+
default_logger().debug(e, exc_info=True)
|
424
|
+
OutputControls().printOutput(
|
425
|
+
colorText.FAIL
|
426
|
+
+ "\n [+] Please enter a valid numeric option & Try Again!"
|
427
|
+
+ colorText.END
|
428
|
+
)
|
429
|
+
|
430
|
+
if not retrial:
|
431
|
+
sleep(2)
|
432
|
+
ConsoleUtility.PKConsoleTools.clearScreen(forceTop=True)
|
433
|
+
return self.init_post_level1_execution(index_option, execute_option, retrial=True)
|
434
|
+
|
435
|
+
return index_option, execute_option
|
436
|
+
|
437
|
+
def update_menu_choice_hierarchy(self):
|
438
|
+
"""
|
439
|
+
Update the menu choice hierarchy string based on current selections.
|
440
|
+
"""
|
441
|
+
try:
|
442
|
+
self.menu_choice_hierarchy = f'{level0MenuDict[self.selected_choice["0"]].strip()}'
|
443
|
+
top_level_menu_dict = level1_X_MenuDict if self.selected_choice["0"] not in "P" else level1_P_MenuDict
|
444
|
+
level2_menu_dict = level2_X_MenuDict if self.selected_choice["0"] not in "P" else level2_P_MenuDict
|
445
|
+
|
446
|
+
if len(self.selected_choice["1"]) > 0:
|
447
|
+
self.menu_choice_hierarchy = f'{self.menu_choice_hierarchy}>{top_level_menu_dict[self.selected_choice["1"]].strip()}'
|
448
|
+
|
449
|
+
if len(self.selected_choice["2"]) > 0:
|
450
|
+
self.menu_choice_hierarchy = f'{self.menu_choice_hierarchy}>{level2_menu_dict[self.selected_choice["2"]].strip()}'
|
451
|
+
|
452
|
+
if self.selected_choice["0"] not in "P":
|
453
|
+
if self.selected_choice["2"] == "6":
|
454
|
+
self.menu_choice_hierarchy = (
|
455
|
+
self.menu_choice_hierarchy
|
456
|
+
+ f'>{level3_X_Reversal_MenuDict[self.selected_choice["3"]].strip()}'
|
457
|
+
)
|
458
|
+
|
459
|
+
if len(self.selected_choice) >= 5 and self.selected_choice["3"] in ["7", "10"]:
|
460
|
+
self.menu_choice_hierarchy = (
|
461
|
+
self.menu_choice_hierarchy
|
462
|
+
+ f'>{level4_X_Lorenzian_MenuDict[self.selected_choice["4"]].strip()}'
|
463
|
+
)
|
464
|
+
|
465
|
+
elif self.selected_choice["2"] in ["30"]:
|
466
|
+
if len(self.selected_choice) >= 3:
|
467
|
+
self.menu_choice_hierarchy = (
|
468
|
+
self.menu_choice_hierarchy
|
469
|
+
+ f'>{level4_X_Lorenzian_MenuDict[self.selected_choice["3"]].strip()}'
|
470
|
+
)
|
471
|
+
|
472
|
+
elif self.selected_choice["2"] == "7":
|
473
|
+
self.menu_choice_hierarchy = (
|
474
|
+
self.menu_choice_hierarchy
|
475
|
+
+ f'>{level3_X_ChartPattern_MenuDict[self.selected_choice["3"]].strip()}'
|
476
|
+
)
|
477
|
+
|
478
|
+
if len(self.selected_choice) >= 5 and self.selected_choice["3"] == "3":
|
479
|
+
self.menu_choice_hierarchy = (
|
480
|
+
self.menu_choice_hierarchy
|
481
|
+
+ f'>{level4_X_ChartPattern_Confluence_MenuDict[self.selected_choice["4"]].strip()}'
|
482
|
+
)
|
483
|
+
elif len(self.selected_choice) >= 5 and self.selected_choice["3"] == "6":
|
484
|
+
self.menu_choice_hierarchy = (
|
485
|
+
self.menu_choice_hierarchy
|
486
|
+
+ f'>{level4_X_ChartPattern_BBands_SQZ_MenuDict[self.selected_choice["4"]].strip()}'
|
487
|
+
)
|
488
|
+
elif len(self.selected_choice) >= 5 and self.selected_choice["3"] == "9":
|
489
|
+
self.menu_choice_hierarchy = (
|
490
|
+
self.menu_choice_hierarchy
|
491
|
+
+ f'>{level4_X_ChartPattern_MASignalMenuDict[self.selected_choice["4"]].strip()}'
|
492
|
+
)
|
493
|
+
elif len(self.selected_choice) >= 5 and self.selected_choice["3"] == "7":
|
494
|
+
self.menu_choice_hierarchy = (
|
495
|
+
self.menu_choice_hierarchy
|
496
|
+
+ f'>{CANDLESTICK_DICT[self.selected_choice["4"]].strip() if self.selected_choice["4"] != 0 else "No Filter"}'
|
497
|
+
)
|
498
|
+
|
499
|
+
elif self.selected_choice["2"] == "21":
|
500
|
+
self.menu_choice_hierarchy = (
|
501
|
+
self.menu_choice_hierarchy
|
502
|
+
+ f'>{level3_X_PopularStocks_MenuDict[self.selected_choice["3"]].strip()}'
|
503
|
+
)
|
504
|
+
|
505
|
+
elif self.selected_choice["2"] == "33":
|
506
|
+
self.menu_choice_hierarchy = (
|
507
|
+
self.menu_choice_hierarchy
|
508
|
+
+ f'>{level3_X_PotentialProfitable_MenuDict[self.selected_choice["3"]].strip()}'
|
509
|
+
)
|
510
|
+
|
511
|
+
elif self.selected_choice["2"] == "40":
|
512
|
+
self.menu_choice_hierarchy = (
|
513
|
+
self.menu_choice_hierarchy
|
514
|
+
+ f'>{PRICE_CROSS_SMA_EMA_DIRECTION_MENUDICT[self.selected_choice["3"]].strip()}'
|
515
|
+
)
|
516
|
+
|
517
|
+
self.menu_choice_hierarchy = (
|
518
|
+
self.menu_choice_hierarchy
|
519
|
+
+ f'>{PRICE_CROSS_SMA_EMA_TYPE_MENUDICT[self.selected_choice["4"]].strip()}'
|
520
|
+
)
|
521
|
+
|
522
|
+
elif self.selected_choice["2"] == "41":
|
523
|
+
self.menu_choice_hierarchy = (
|
524
|
+
self.menu_choice_hierarchy
|
525
|
+
+ f'>{PRICE_CROSS_PIVOT_POINT_TYPE_MENUDICT[self.selected_choice["3"]].strip()}'
|
526
|
+
)
|
527
|
+
|
528
|
+
self.menu_choice_hierarchy = (
|
529
|
+
self.menu_choice_hierarchy
|
530
|
+
+ f'>{PRICE_CROSS_SMA_EMA_DIRECTION_MENUDICT[self.selected_choice["4"]].strip()}'
|
531
|
+
)
|
532
|
+
|
533
|
+
intraday = "(Intraday)" if ("Intraday" not in self.menu_choice_hierarchy and (self.user_passed_args is not None and self.user_passed_args.intraday) or self.config_manager.isIntradayConfig()) else ""
|
534
|
+
self.menu_choice_hierarchy = f"{self.menu_choice_hierarchy}{intraday}"
|
535
|
+
|
536
|
+
self.menu_choice_hierarchy = self.menu_choice_hierarchy.replace("N-", f"{self.n_value_for_menu}-")
|
537
|
+
except:
|
538
|
+
pass
|
539
|
+
|
540
|
+
ConsoleUtility.PKConsoleTools.clearScreen(forceTop=True)
|
541
|
+
needs_calc = self.user_passed_args is not None and self.user_passed_args.backtestdaysago is not None
|
542
|
+
past_date = f"[ {PKDateUtilities.nthPastTradingDateStringFromFutureDate(int(self.user_passed_args.backtestdaysago) if needs_calc else 0)} ]" if needs_calc else ""
|
543
|
+
|
544
|
+
report_title = f"{self.user_passed_args.pipedtitle}|" if self.user_passed_args is not None and self.user_passed_args.pipedtitle is not None else ""
|
545
|
+
run_option_name = PKScanRunner.getFormattedChoices(self.user_passed_args, self.selected_choice)
|
546
|
+
|
547
|
+
if ((":0:" in run_option_name or "_0_" in run_option_name) and self.user_passed_args.progressstatus is not None) or self.user_passed_args.progressstatus is not None:
|
548
|
+
run_option_name = self.user_passed_args.progressstatus.split("=>")[0].split(" [+] ")[1].strip()
|
549
|
+
|
550
|
+
report_title = f"{run_option_name} | {report_title}" if run_option_name is not None else report_title
|
551
|
+
|
552
|
+
if len(run_option_name) >= 5:
|
553
|
+
PKAnalyticsService().send_event(run_option_name)
|
554
|
+
|
555
|
+
OutputControls().printOutput(
|
556
|
+
colorText.FAIL
|
557
|
+
+ f" [+] You chose: {report_title} "
|
558
|
+
+ self.menu_choice_hierarchy
|
559
|
+
+ (f" (Piped Scan Mode) [{self.user_passed_args.pipedmenus}] {past_date}" if (self.user_passed_args is not None and self.user_passed_args.pipedmenus is not None) else "")
|
560
|
+
+ colorText.END
|
561
|
+
)
|
562
|
+
|
563
|
+
default_logger().info(self.menu_choice_hierarchy)
|
564
|
+
return self.menu_choice_hierarchy
|
565
|
+
|
566
|
+
def show_option_error_message(self):
|
567
|
+
"""Display an error message for invalid menu options."""
|
568
|
+
OutputControls().printOutput(
|
569
|
+
colorText.FAIL
|
570
|
+
+ "\n [+] Please enter a valid option & try Again!"
|
571
|
+
+ colorText.END
|
572
|
+
)
|
573
|
+
sleep(2)
|
574
|
+
ConsoleUtility.PKConsoleTools.clearScreen(forceTop=True)
|
575
|
+
|
576
|
+
def handle_secondary_menu_choices(self, menu_option, testing=False, default_answer=None, user=None):
|
577
|
+
"""
|
578
|
+
Handle secondary menu choices (help, update, config, etc.).
|
579
|
+
|
580
|
+
Args:
|
581
|
+
menu_option: Selected menu option
|
582
|
+
testing (bool): Whether running in test mode
|
583
|
+
default_answer: Default answer for prompts
|
584
|
+
user: User identifier
|
585
|
+
"""
|
586
|
+
if menu_option == "H":
|
587
|
+
self.show_send_help_info(default_answer, user)
|
588
|
+
elif menu_option == "U":
|
589
|
+
OTAUpdater.checkForUpdate(VERSION, skipDownload=testing)
|
590
|
+
if default_answer is None:
|
591
|
+
OutputControls().takeUserInput("Press <Enter> to continue...")
|
592
|
+
elif menu_option == "T":
|
593
|
+
if self.user_passed_args is None or self.user_passed_args.options is None:
|
594
|
+
selected_menu = self.m0.find(menu_option)
|
595
|
+
self.m1.renderForMenu(selected_menu=selected_menu)
|
596
|
+
period_option = input(
|
597
|
+
colorText.FAIL + " [+] Select option: "
|
598
|
+
) or ('L' if self.config_manager.period == '1y' else 'S')
|
599
|
+
OutputControls().printOutput(colorText.END, end="")
|
600
|
+
|
601
|
+
if period_option is None or period_option.upper() not in ["L", "S", "B"]:
|
602
|
+
return
|
603
|
+
|
604
|
+
ConsoleUtility.PKConsoleTools.clearScreen(forceTop=True)
|
605
|
+
|
606
|
+
if period_option.upper() in ["L", "S"]:
|
607
|
+
selected_menu = self.m1.find(period_option)
|
608
|
+
self.m2.renderForMenu(selected_menu=selected_menu)
|
609
|
+
duration_option = input(
|
610
|
+
colorText.FAIL + " [+] Select option: "
|
611
|
+
) or "1"
|
612
|
+
OutputControls().printOutput(colorText.END, end="")
|
613
|
+
|
614
|
+
if duration_option is None or duration_option.upper() not in ["1", "2", "3", "4", "5"]:
|
615
|
+
return
|
616
|
+
|
617
|
+
ConsoleUtility.PKConsoleTools.clearScreen(forceTop=True)
|
618
|
+
|
619
|
+
if duration_option.upper() in ["1", "2", "3", "4"]:
|
620
|
+
selected_menu = self.m2.find(duration_option)
|
621
|
+
period_durations = selected_menu.menuText.split("(")[1].split(")")[0].split(", ")
|
622
|
+
self.config_manager.period = period_durations[0]
|
623
|
+
self.config_manager.duration = period_durations[1]
|
624
|
+
self.config_manager.setConfig(ConfigManager.parser, default=True, showFileCreatedText=False)
|
625
|
+
self.config_manager.deleteFileWithPattern(rootDir=Archiver.get_user_data_dir(), pattern="*stock_data_*.pkl*")
|
626
|
+
elif duration_option.upper() in ["5"]:
|
627
|
+
self.config_manager.setConfig(ConfigManager.parser, default=False, showFileCreatedText=True)
|
628
|
+
self.config_manager.deleteFileWithPattern(rootDir=Archiver.get_user_data_dir(), pattern="*stock_data_*.pkl*")
|
629
|
+
return
|
630
|
+
elif period_option.upper() in ["B"]:
|
631
|
+
last_trading_date = PKDateUtilities.nthPastTradingDateStringFromFutureDate(n=(22 if self.config_manager.period == '1y' else 15))
|
632
|
+
backtest_days_ago = input(
|
633
|
+
f"{colorText.FAIL} [+] Enter no. of days/candles in the past as starting candle for which you'd like to run the scans\n [+] You can also enter a past date in {colorText.END}{colorText.GREEN}YYYY-MM-DD{colorText.END}{colorText.FAIL} format\n [+] (e.g. {colorText.GREEN}10{colorText.END} for 10 candles ago or {colorText.GREEN}0{colorText.END} for today or {colorText.GREEN}{last_trading_date}{colorText.END}):"
|
634
|
+
) or ('22' if self.config_manager.period == '1y' else '15')
|
635
|
+
OutputControls().printOutput(colorText.END, end="")
|
636
|
+
|
637
|
+
if len(str(backtest_days_ago)) >= 3 and "-" in str(backtest_days_ago):
|
638
|
+
try:
|
639
|
+
backtest_days_ago = abs(PKDateUtilities.trading_days_between(
|
640
|
+
d1=PKDateUtilities.dateFromYmdString(str(backtest_days_ago)),
|
641
|
+
d2=PKDateUtilities.currentDateTime()))
|
642
|
+
except Exception as e:
|
643
|
+
default_logger().debug(e, exc_info=True)
|
644
|
+
OutputControls().printOutput(f"An error occured! Going ahead with default inputs.")
|
645
|
+
backtest_days_ago = ('22' if self.config_manager.period == '1y' else '15')
|
646
|
+
sleep(3)
|
647
|
+
pass
|
648
|
+
|
649
|
+
launcher = f'"{sys.argv[0]}"' if " " in sys.argv[0] else sys.argv[0]
|
650
|
+
requesting_user = f" -u {self.user_passed_args.user}" if self.user_passed_args.user is not None else ""
|
651
|
+
enable_log = f" -l" if self.user_passed_args.log else ""
|
652
|
+
enable_telegram_mode = f" --telegram" if self.user_passed_args is not None and self.user_passed_args.telegram else ""
|
653
|
+
stock_list_param = f" --stocklist {self.user_passed_args.stocklist}" if self.user_passed_args.stocklist else ""
|
654
|
+
slicewindow_param = f" --slicewindow {self.user_passed_args.slicewindow}" if self.user_passed_args.slicewindow else ""
|
655
|
+
fname_param = f" --fname {self.results_contents_encoded}" if self.results_contents_encoded else ""
|
656
|
+
launcher = f"python3.12 {launcher}" if (launcher.endswith(".py\"") or launcher.endswith(".py")) else launcher
|
657
|
+
|
658
|
+
OutputControls().printOutput(f"{colorText.GREEN}Launching PKScreener in quick backtest mode. If it does not launch, please try with the following:{colorText.END}\n{colorText.FAIL}{launcher} --backtestdaysago {int(backtest_days_ago)}{requesting_user}{enable_log}{enable_telegram_mode}{stock_list_param}{slicewindow_param}{fname_param}{colorText.END}\n{colorText.WARN}Press Ctrl + C to exit quick backtest mode.{colorText.END}")
|
659
|
+
sleep(2)
|
660
|
+
os.system(f"{launcher} --systemlaunched -a Y -e --backtestdaysago {int(backtest_days_ago)}{requesting_user}{enable_log}{enable_telegram_mode}{stock_list_param}{slicewindow_param}{fname_param}")
|
661
|
+
ConsoleUtility.PKConsoleTools.clearScreen(clearAlways=True, forceTop=True)
|
662
|
+
return None, None
|
663
|
+
elif self.user_passed_args is not None and self.user_passed_args.options is not None:
|
664
|
+
options = self.user_passed_args.options.split(":")
|
665
|
+
selected_menu = self.m0.find(options[0])
|
666
|
+
self.m1.renderForMenu(selected_menu=selected_menu, asList=True)
|
667
|
+
selected_menu = self.m1.find(options[1])
|
668
|
+
self.m2.renderForMenu(selected_menu=selected_menu, asList=True)
|
669
|
+
|
670
|
+
if options[2] in ["1", "2", "3", "4"]:
|
671
|
+
selected_menu = self.m2.find(options[2])
|
672
|
+
period_durations = selected_menu.menuText.split("(")[1].split(")")[0].split(", ")
|
673
|
+
self.config_manager.period = period_durations[0]
|
674
|
+
self.config_manager.duration = period_durations[1]
|
675
|
+
self.config_manager.setConfig(ConfigManager.parser, default=True, showFileCreatedText=False)
|
676
|
+
else:
|
677
|
+
self.toggle_user_config()
|
678
|
+
else:
|
679
|
+
self.toggle_user_config()
|
680
|
+
elif menu_option == "E":
|
681
|
+
self.config_manager.setConfig(ConfigManager.parser)
|
682
|
+
elif menu_option == "Y":
|
683
|
+
self.show_send_config_info(default_answer, user)
|
684
|
+
|
685
|
+
return
|
686
|
+
|
687
|
+
def show_send_config_info(self, default_answer=None, user=None):
|
688
|
+
"""
|
689
|
+
Show and send configuration information.
|
690
|
+
|
691
|
+
Args:
|
692
|
+
default_answer: Default answer for prompts
|
693
|
+
user: User identifier
|
694
|
+
"""
|
695
|
+
config_data = self.config_manager.showConfigFile(default_answer=('Y' if user is not None else default_answer))
|
696
|
+
|
697
|
+
if user is not None:
|
698
|
+
self.send_message_to_telegram_channel(message=ImageUtility.PKImageTools.removeAllColorStyles(config_data), user=user)
|
699
|
+
|
700
|
+
if default_answer is None:
|
701
|
+
input("Press any key to continue...")
|
702
|
+
|
703
|
+
def show_send_help_info(self, default_answer=None, user=None):
|
704
|
+
"""
|
705
|
+
Show and send help information.
|
706
|
+
|
707
|
+
Args:
|
708
|
+
default_answer: Default answer for prompts
|
709
|
+
user: User identifier
|
710
|
+
"""
|
711
|
+
help_data = ConsoleUtility.PKConsoleTools.showDevInfo(default_answer=('Y' if user is not None else default_answer))
|
712
|
+
|
713
|
+
if user is not None:
|
714
|
+
self.send_message_to_telegram_channel(message=ImageUtility.PKImageTools.removeAllColorStyles(help_data), user=user)
|
715
|
+
|
716
|
+
if default_answer is None:
|
717
|
+
input("Press any key to continue...")
|
718
|
+
|
719
|
+
def toggle_user_config(self):
|
720
|
+
"""Toggle user configuration between intraday and daily modes."""
|
721
|
+
self.config_manager.toggleConfig(
|
722
|
+
candleDuration="1d" if self.config_manager.isIntradayConfig() else "1m"
|
723
|
+
)
|
724
|
+
OutputControls().printOutput(
|
725
|
+
colorText.GREEN
|
726
|
+
+ "\nConfiguration toggled to duration: "
|
727
|
+
+ str(self.config_manager.duration)
|
728
|
+
+ " and period: "
|
729
|
+
+ str(self.config_manager.period)
|
730
|
+
+ colorText.END
|
731
|
+
)
|
732
|
+
input("\nPress <Enter> to Continue...\n")
|
733
|
+
|
734
|
+
|
735
|
+
class ScanExecutor:
|
736
|
+
"""
|
737
|
+
Executes scanning operations including stock screening, backtesting, and data processing.
|
738
|
+
Manages worker processes, task queues, and result collection.
|
739
|
+
"""
|
740
|
+
|
741
|
+
def __init__(self, config_manager, user_passed_args):
|
742
|
+
"""
|
743
|
+
Initialize the ScanExecutor with configuration and user arguments.
|
744
|
+
|
745
|
+
Args:
|
746
|
+
config_manager: Configuration manager instance
|
747
|
+
user_passed_args: User passed arguments
|
748
|
+
"""
|
749
|
+
self.config_manager = config_manager
|
750
|
+
self.user_passed_args = user_passed_args
|
751
|
+
self.fetcher = Fetcher.screenerStockDataFetcher(self.config_manager)
|
752
|
+
self.screener = ScreeningStatistics.ScreeningStatistics(self.config_manager, default_logger())
|
753
|
+
self.tasks_queue = None
|
754
|
+
self.results_queue = None
|
755
|
+
self.consumers = None
|
756
|
+
self.logging_queue = None
|
757
|
+
self.mp_manager = None
|
758
|
+
self.keyboard_interrupt_event = None
|
759
|
+
self.keyboard_interrupt_event_fired = False
|
760
|
+
self.screen_counter = None
|
761
|
+
self.screen_results_counter = None
|
762
|
+
self.elapsed_time = 0
|
763
|
+
self.start_time = 0
|
764
|
+
self.scan_cycle_running = False
|
765
|
+
|
766
|
+
def run_scanners(self, menu_option, items, tasks_queue, results_queue, num_stocks,
|
767
|
+
backtest_period, iterations, consumers, screen_results, save_results,
|
768
|
+
backtest_df, testing=False):
|
769
|
+
"""
|
770
|
+
Execute scanning operations with the given parameters.
|
771
|
+
|
772
|
+
Args:
|
773
|
+
menu_option: Selected menu option
|
774
|
+
items: List of items to scan
|
775
|
+
tasks_queue: Tasks queue for multiprocessing
|
776
|
+
results_queue: Results queue for multiprocessing
|
777
|
+
num_stocks: Number of stocks to process
|
778
|
+
backtest_period: Backtest period in days
|
779
|
+
iterations: Number of iterations
|
780
|
+
consumers: Consumer processes
|
781
|
+
screen_results: Screen results container
|
782
|
+
save_results: Save results container
|
783
|
+
backtest_df: Backtest dataframe
|
784
|
+
testing (bool): Whether running in test mode
|
785
|
+
|
786
|
+
Returns:
|
787
|
+
tuple: Screen results, save results, and backtest dataframe
|
788
|
+
"""
|
789
|
+
result = None
|
790
|
+
backtest_df = None
|
791
|
+
review_date = self.get_review_date() if self.criteria_date_time is None else self.criteria_date_time
|
792
|
+
max_allowed = self.get_max_allowed_results_count(iterations, testing)
|
793
|
+
|
794
|
+
try:
|
795
|
+
original_number_of_stocks = num_stocks
|
796
|
+
iterations, num_stocks_per_iteration = self.get_iterations_and_stock_counts(num_stocks, iterations)
|
797
|
+
|
798
|
+
OutputControls().printOutput(
|
799
|
+
colorText.GREEN
|
800
|
+
+ f" [+] For {review_date}, total {'Scanners' if menu_option in ['F'] else 'Stocks'} under review: {num_stocks} over {iterations} iterations..."
|
801
|
+
+ colorText.END
|
802
|
+
)
|
803
|
+
|
804
|
+
if not self.user_passed_args.download:
|
805
|
+
OutputControls().printOutput(colorText.WARN
|
806
|
+
+ f" [+] Starting {'Stock' if menu_option not in ['C'] else 'Intraday'} {'Screening' if menu_option=='X' else ('Analysis' if menu_option == 'C' else 'Look-up' if menu_option in ['F'] else 'Backtesting.')}. Press Ctrl+C to stop!"
|
807
|
+
+ colorText.END
|
808
|
+
)
|
809
|
+
|
810
|
+
if self.user_passed_args.progressstatus is not None:
|
811
|
+
OutputControls().printOutput(f"{colorText.GREEN}{self.user_passed_args.progressstatus}{colorText.END}")
|
812
|
+
else:
|
813
|
+
OutputControls().printOutput(
|
814
|
+
colorText.FAIL
|
815
|
+
+ f" [+] Download ONLY mode (OHLCV for period:{self.config_manager.period}, candle-duration:{self.config_manager.duration} )! Stocks will not be screened!"
|
816
|
+
+ colorText.END
|
817
|
+
)
|
818
|
+
|
819
|
+
bar, spinner = Utility.tools.getProgressbarStyle()
|
820
|
+
|
821
|
+
with alive_bar(num_stocks, bar=bar, spinner=spinner) as progressbar:
|
822
|
+
lstscreen = []
|
823
|
+
lstsave = []
|
824
|
+
result = None
|
825
|
+
backtest_df = None
|
826
|
+
self.start_time = time.time() if not self.scan_cycle_running else self.start_time
|
827
|
+
self.scan_cycle_running = True
|
828
|
+
|
829
|
+
def process_results_callback(result_item, processed_count, result_df, *other_args):
|
830
|
+
(menu_option, backtest_period, result, lstscreen, lstsave) = other_args
|
831
|
+
num_stocks = processed_count
|
832
|
+
result = result_item
|
833
|
+
backtest_df = self.process_results(menu_option, backtest_period, result, lstscreen, lstsave, result_df)
|
834
|
+
progressbar()
|
835
|
+
progressbar.text(
|
836
|
+
colorText.GREEN
|
837
|
+
+ f"{'Remaining' if self.user_passed_args.download else ('Found' if menu_option in ['X','F'] else 'Analysed')} {len(lstscreen) if not self.user_passed_args.download else processed_count} {'Stocks' if menu_option in ['X'] else 'Records'}"
|
838
|
+
+ colorText.END
|
839
|
+
)
|
840
|
+
|
841
|
+
if result is not None:
|
842
|
+
if not self.user_passed_args.monitor and len(lstscreen) > 0 and self.user_passed_args is not None and self.user_passed_args.options.split(":")[2] in ["29"]:
|
843
|
+
scr_df = pd.DataFrame(lstscreen)
|
844
|
+
existing_columns = ["Stock","%Chng","LTP","volume"]
|
845
|
+
new_columns = ["BidQty","AskQty","LwrCP","UprCP","VWAP","DayVola","Del(%)"]
|
846
|
+
existing_columns.extend(new_columns)
|
847
|
+
scr_df = scr_df[existing_columns]
|
848
|
+
scr_df.sort_values(by=["volume","BidQty"], ascending=False, inplace=True)
|
849
|
+
tabulated_results = colorText.miniTabulator().tb.tabulate(
|
850
|
+
scr_df,
|
851
|
+
headers="keys",
|
852
|
+
showindex=False,
|
853
|
+
tablefmt=colorText.No_Pad_GridFormat,
|
854
|
+
maxcolwidths=Utility.tools.getMaxColumnWidths(scr_df)
|
855
|
+
)
|
856
|
+
table_length = 2*len(lstscreen)+5
|
857
|
+
OutputControls().printOutput('\n'+tabulated_results)
|
858
|
+
OutputControls().moveCursorUpLines(table_length)
|
859
|
+
|
860
|
+
if self.keyboard_interrupt_event_fired:
|
861
|
+
return False, backtest_df
|
862
|
+
return not ((testing and len(lstscreen) >= 1) or len(lstscreen) >= max_allowed), backtest_df
|
863
|
+
|
864
|
+
other_args = (menu_option, backtest_period, result, lstscreen, lstsave)
|
865
|
+
backtest_df, result = PKScanRunner.runScan(self.user_passed_args, testing, num_stocks, iterations, items, num_stocks_per_iteration, tasks_queue, results_queue, original_number_of_stocks, backtest_df, *other_args, resultsReceivedCb=process_results_callback)
|
866
|
+
|
867
|
+
OutputControls().moveCursorUpLines(3 if OutputControls().enableMultipleLineOutput else 1)
|
868
|
+
self.elapsed_time = time.time() - self.start_time
|
869
|
+
|
870
|
+
if menu_option in ["X", "G", "C", "F"]:
|
871
|
+
screen_results = pd.DataFrame(lstscreen)
|
872
|
+
save_results = pd.DataFrame(lstsave)
|
873
|
+
|
874
|
+
except KeyboardInterrupt:
|
875
|
+
try:
|
876
|
+
self.keyboard_interrupt_event.set()
|
877
|
+
self.keyboard_interrupt_event_fired = True
|
878
|
+
OutputControls().printOutput(
|
879
|
+
colorText.FAIL
|
880
|
+
+ "\n [+] Terminating Script, Please wait..."
|
881
|
+
+ colorText.END
|
882
|
+
)
|
883
|
+
PKScanRunner.terminateAllWorkers(userPassedArgs=self.user_passed_args, consumers=consumers, tasks_queue=tasks_queue, testing=testing)
|
884
|
+
logging.shutdown()
|
885
|
+
except KeyboardInterrupt:
|
886
|
+
pass
|
887
|
+
except Exception as e:
|
888
|
+
default_logger().debug(e, exc_info=True)
|
889
|
+
OutputControls().printOutput(
|
890
|
+
colorText.FAIL
|
891
|
+
+ f"\nException:\n{e}\n [+] Terminating Script, Please wait..."
|
892
|
+
+ colorText.END
|
893
|
+
)
|
894
|
+
PKScanRunner.terminateAllWorkers(userPassedArgs=self.user_passed_args, consumers=consumers, tasks_queue=tasks_queue, testing=testing)
|
895
|
+
logging.shutdown()
|
896
|
+
|
897
|
+
if result is not None and len(result) >=1 and self.criteria_date_time is None:
|
898
|
+
if self.user_passed_args is not None and self.user_passed_args.backtestdaysago is not None:
|
899
|
+
self.criteria_date_time = result[2].copy().index[-1-int(self.user_passed_args.backtestdaysago)]
|
900
|
+
else:
|
901
|
+
self.criteria_date_time = result[2].copy().index[-1] if self.user_passed_args.slicewindow is None else datetime.strptime(self.user_passed_args.slicewindow.replace("'",""),"%Y-%m-%d %H:%M:%S.%f%z")
|
902
|
+
local_tz = datetime.now(UTC).astimezone().tzinfo
|
903
|
+
exchange_tz = PKDateUtilities.currentDateTime().astimezone().tzinfo
|
904
|
+
|
905
|
+
if local_tz != exchange_tz:
|
906
|
+
self.criteria_date_time = PKDateUtilities.utc_to_ist(self.criteria_date_time, localTz=local_tz)
|
907
|
+
|
908
|
+
if result is not None and len(result) >=1 and "Date" not in save_results.columns:
|
909
|
+
temp_df = result[2].copy()
|
910
|
+
temp_df.reset_index(inplace=True)
|
911
|
+
temp_df = temp_df.tail(1)
|
912
|
+
temp_df.rename(columns={"index": "Date"}, inplace=True)
|
913
|
+
target_date = (
|
914
|
+
temp_df["Date"].iloc[0]
|
915
|
+
if "Date" in temp_df.columns
|
916
|
+
else str(temp_df.iloc[:, 0][0])
|
917
|
+
)
|
918
|
+
save_results["Date"] = str(target_date).split(" ")[0]
|
919
|
+
|
920
|
+
return screen_results, save_results, backtest_df
|
921
|
+
|
922
|
+
def process_results(self, menu_option, backtest_period, result, lstscreen, lstsave, backtest_df):
|
923
|
+
"""
|
924
|
+
Process scanning results and update data structures.
|
925
|
+
|
926
|
+
Args:
|
927
|
+
menu_option: Selected menu option
|
928
|
+
backtest_period: Backtest period in days
|
929
|
+
result: Result data
|
930
|
+
lstscreen: Screen results list
|
931
|
+
lstsave: Save results list
|
932
|
+
backtest_df: Backtest dataframe
|
933
|
+
|
934
|
+
Returns:
|
935
|
+
DataFrame: Updated backtest dataframe
|
936
|
+
"""
|
937
|
+
if result is not None:
|
938
|
+
lstscreen.append(result[0])
|
939
|
+
lstsave.append(result[1])
|
940
|
+
sample_days = result[4]
|
941
|
+
|
942
|
+
if menu_option == "B":
|
943
|
+
backtest_df = self.update_backtest_results(
|
944
|
+
backtest_period, self.start_time, result, sample_days, backtest_df
|
945
|
+
)
|
946
|
+
|
947
|
+
self.elapsed_time = time.time() - self.start_time
|
948
|
+
|
949
|
+
return backtest_df
|
950
|
+
|
951
|
+
def update_backtest_results(self, backtest_period, start_time, result, sample_days, backtest_df):
|
952
|
+
"""
|
953
|
+
Update backtest results with new data.
|
954
|
+
|
955
|
+
Args:
|
956
|
+
backtest_period: Backtest period in days
|
957
|
+
start_time: Start time of operation
|
958
|
+
result: Result data
|
959
|
+
sample_days: Number of sample days
|
960
|
+
backtest_df: Backtest dataframe
|
961
|
+
|
962
|
+
Returns:
|
963
|
+
DataFrame: Updated backtest dataframe
|
964
|
+
"""
|
965
|
+
sell_signal = (
|
966
|
+
str(self.selected_choice["2"]) in ["6", "7"] and str(self.selected_choice["3"]) in ["2"]
|
967
|
+
) or self.selected_choice["2"] in ["15", "16", "19", "25"]
|
968
|
+
|
969
|
+
backtest_df = backtest(
|
970
|
+
result[3],
|
971
|
+
result[2],
|
972
|
+
result[1],
|
973
|
+
result[0],
|
974
|
+
backtest_period,
|
975
|
+
sample_days,
|
976
|
+
backtest_df,
|
977
|
+
sell_signal,
|
978
|
+
)
|
979
|
+
|
980
|
+
self.elapsed_time = time.time() - start_time
|
981
|
+
return backtest_df
|
982
|
+
|
983
|
+
def get_review_date(self):
|
984
|
+
"""
|
985
|
+
Get the review date for scanning operations.
|
986
|
+
|
987
|
+
Returns:
|
988
|
+
str: Review date string
|
989
|
+
"""
|
990
|
+
review_date = PKDateUtilities.tradingDate().strftime('%Y-%m-%d')
|
991
|
+
|
992
|
+
if self.user_passed_args is not None and self.user_passed_args.backtestdaysago is not None:
|
993
|
+
review_date = PKDateUtilities.nthPastTradingDateStringFromFutureDate(int(self.user_passed_args.backtestdaysago))
|
994
|
+
|
995
|
+
return review_date
|
996
|
+
|
997
|
+
def get_max_allowed_results_count(self, iterations, testing):
|
998
|
+
"""
|
999
|
+
Get the maximum allowed results count based on iterations and testing mode.
|
1000
|
+
|
1001
|
+
Args:
|
1002
|
+
iterations: Number of iterations
|
1003
|
+
testing (bool): Whether running in test mode
|
1004
|
+
|
1005
|
+
Returns:
|
1006
|
+
int: Maximum allowed results count
|
1007
|
+
"""
|
1008
|
+
return iterations * (self.config_manager.maxdisplayresults if self.user_passed_args.maxdisplayresults is None else (int(self.user_passed_args.maxdisplayresults) if not testing else 1))
|
1009
|
+
|
1010
|
+
def get_iterations_and_stock_counts(self, num_stocks, iterations):
|
1011
|
+
"""
|
1012
|
+
Calculate iterations and stock counts for scanning operations.
|
1013
|
+
|
1014
|
+
Args:
|
1015
|
+
num_stocks: Number of stocks to process
|
1016
|
+
iterations: Number of iterations
|
1017
|
+
|
1018
|
+
Returns:
|
1019
|
+
tuple: Iterations count and stocks per iteration
|
1020
|
+
"""
|
1021
|
+
if num_stocks <= 2500:
|
1022
|
+
return 1, num_stocks
|
1023
|
+
|
1024
|
+
original_iterations = iterations
|
1025
|
+
ideal_num_stocks_max_per_iteration = 100
|
1026
|
+
iterations = int(num_stocks * iterations / ideal_num_stocks_max_per_iteration) + 1
|
1027
|
+
num_stocks_per_iteration = int(num_stocks / int(iterations))
|
1028
|
+
|
1029
|
+
if num_stocks_per_iteration < 10:
|
1030
|
+
num_stocks_per_iteration = num_stocks if (iterations == 1 or num_stocks <= iterations) else int(num_stocks / int(iterations))
|
1031
|
+
iterations = original_iterations
|
1032
|
+
|
1033
|
+
if num_stocks_per_iteration > 500:
|
1034
|
+
num_stocks_per_iteration = 500
|
1035
|
+
iterations = int(num_stocks / num_stocks_per_iteration) + 1
|
1036
|
+
|
1037
|
+
return iterations, num_stocks_per_iteration
|
1038
|
+
|
1039
|
+
def close_workers_and_exit(self):
|
1040
|
+
"""Close all worker processes and exit the application."""
|
1041
|
+
if self.consumers is not None:
|
1042
|
+
PKScanRunner.terminateAllWorkers(userPassedArgs=self.user_passed_args, consumers=self.consumers,
|
1043
|
+
tasks_queue=self.tasks_queue, testing=self.user_passed_args.testbuild)
|
1044
|
+
|
1045
|
+
|
1046
|
+
class ResultProcessor:
|
1047
|
+
"""
|
1048
|
+
Processes, formats, and displays screening results.
|
1049
|
+
Handles data labeling, filtering, and presentation of scan outcomes.
|
1050
|
+
"""
|
1051
|
+
|
1052
|
+
def __init__(self, config_manager, user_passed_args):
|
1053
|
+
"""
|
1054
|
+
Initialize the ResultProcessor with configuration and user arguments.
|
1055
|
+
|
1056
|
+
Args:
|
1057
|
+
config_manager: Configuration manager instance
|
1058
|
+
user_passed_args: User passed arguments
|
1059
|
+
"""
|
1060
|
+
self.config_manager = config_manager
|
1061
|
+
self.user_passed_args = user_passed_args
|
1062
|
+
self.saved_screen_results = None
|
1063
|
+
self.show_saved_diff_results = False
|
1064
|
+
self.results_contents_encoded = None
|
1065
|
+
self.criteria_date_time = None
|
1066
|
+
|
1067
|
+
def label_data_for_printing(self, screen_results, save_results, volume_ratio, execute_option, reversal_option, menu_option):
|
1068
|
+
"""
|
1069
|
+
Label and format data for printing and display.
|
1070
|
+
|
1071
|
+
Args:
|
1072
|
+
screen_results: Screen results data
|
1073
|
+
save_results: Results to save
|
1074
|
+
volume_ratio: Volume ratio for formatting
|
1075
|
+
execute_option: Execute option value
|
1076
|
+
reversal_option: Reversal option value
|
1077
|
+
menu_option: Menu option value
|
1078
|
+
|
1079
|
+
Returns:
|
1080
|
+
tuple: Formatted screen results and save results
|
1081
|
+
"""
|
1082
|
+
if save_results is None:
|
1083
|
+
return screen_results, save_results
|
1084
|
+
|
1085
|
+
try:
|
1086
|
+
is_trading = PKDateUtilities.isTradingTime() and not PKDateUtilities.isTodayHoliday()[0]
|
1087
|
+
|
1088
|
+
if "RUNNER" not in os.environ.keys() and (is_trading or self.user_passed_args.monitor or ("RSIi" in save_results.columns)) and self.config_manager.calculatersiintraday:
|
1089
|
+
screen_results['RSI'] = screen_results['RSI'].astype(str) + "/" + screen_results['RSIi'].astype(str)
|
1090
|
+
save_results['RSI'] = save_results['RSI'].astype(str) + "/" + save_results['RSIi'].astype(str)
|
1091
|
+
screen_results.rename(columns={"RSI": "RSI/i"}, inplace=True)
|
1092
|
+
save_results.rename(columns={"RSI": "RSI/i"}, inplace=True)
|
1093
|
+
|
1094
|
+
sort_key = ["volume"] if "RSI" not in self.menu_choice_hierarchy else ("RSIi" if (is_trading or "RSIi" in save_results.columns) else "RSI")
|
1095
|
+
ascending = [False if "RSI" not in self.menu_choice_hierarchy else True]
|
1096
|
+
|
1097
|
+
if execute_option == 21:
|
1098
|
+
if reversal_option in [3, 5, 6, 7]:
|
1099
|
+
sort_key = ["MFI"]
|
1100
|
+
ascending = [reversal_option in [6, 7]]
|
1101
|
+
elif reversal_option in [8, 9]:
|
1102
|
+
sort_key = ["FVDiff"]
|
1103
|
+
ascending = [reversal_option in [9]]
|
1104
|
+
elif execute_option == 7:
|
1105
|
+
if reversal_option in [3]:
|
1106
|
+
if "SuperConfSort" in save_results.columns:
|
1107
|
+
sort_key = ["SuperConfSort"]
|
1108
|
+
ascending = [False]
|
1109
|
+
else:
|
1110
|
+
sort_key = ["volume"]
|
1111
|
+
ascending = [False]
|
1112
|
+
elif reversal_option in [4]:
|
1113
|
+
if "deviationScore" in save_results.columns:
|
1114
|
+
sort_key = ["deviationScore"]
|
1115
|
+
ascending = [True]
|
1116
|
+
else:
|
1117
|
+
sort_key = ["volume"]
|
1118
|
+
ascending = [False]
|
1119
|
+
elif execute_option == 23:
|
1120
|
+
sort_key = ["bbands_ulr_ratio_max5"] if "bbands_ulr_ratio_max5" in screen_results.columns else ["volume"]
|
1121
|
+
ascending = [False]
|
1122
|
+
elif execute_option == 27: # ATR Cross
|
1123
|
+
sort_key = ["ATR"] if "ATR" in screen_results.columns else ["volume"]
|
1124
|
+
ascending = [False]
|
1125
|
+
elif execute_option == 31: # DEEL Momentum
|
1126
|
+
sort_key = ["%Chng"]
|
1127
|
+
ascending = [False]
|
1128
|
+
|
1129
|
+
try:
|
1130
|
+
try:
|
1131
|
+
screen_results[sort_key] = screen_results[sort_key].replace("", np.nan).replace(np.inf, np.nan).replace(-np.inf, np.nan).astype(float)
|
1132
|
+
except:
|
1133
|
+
pass
|
1134
|
+
|
1135
|
+
try:
|
1136
|
+
save_results[sort_key] = save_results[sort_key].replace("", np.nan).replace(np.inf, np.nan).replace(-np.inf, np.nan).astype(float)
|
1137
|
+
except:
|
1138
|
+
pass
|
1139
|
+
|
1140
|
+
screen_results.sort_values(by=sort_key, ascending=ascending, inplace=True)
|
1141
|
+
save_results.sort_values(by=sort_key, ascending=ascending, inplace=True)
|
1142
|
+
except Exception as e:
|
1143
|
+
default_logger().debug(e, exc_info=True)
|
1144
|
+
pass
|
1145
|
+
|
1146
|
+
columns_to_be_deleted = ["MFI", "FVDiff", "ConfDMADifference", "bbands_ulr_ratio_max5", "RSIi"]
|
1147
|
+
|
1148
|
+
if menu_option not in ["F"]:
|
1149
|
+
columns_to_be_deleted.extend(["ScanOption"])
|
1150
|
+
|
1151
|
+
if "EoDDiff" in save_results.columns:
|
1152
|
+
columns_to_be_deleted.extend(["Trend", "Breakout"])
|
1153
|
+
|
1154
|
+
if "SuperConfSort" in save_results.columns:
|
1155
|
+
columns_to_be_deleted.extend(["SuperConfSort"])
|
1156
|
+
|
1157
|
+
if "deviationScore" in save_results.columns:
|
1158
|
+
columns_to_be_deleted.extend(["deviationScore"])
|
1159
|
+
|
1160
|
+
if self.user_passed_args is not None and self.user_passed_args.options is not None and self.user_passed_args.options.upper().startswith("C"):
|
1161
|
+
columns_to_be_deleted.append("FairValue")
|
1162
|
+
|
1163
|
+
if execute_option == 27 and "ATR" in screen_results.columns: # ATR Cross
|
1164
|
+
screen_results['ATR'] = screen_results['ATR'].astype(str)
|
1165
|
+
screen_results['ATR'] = colorText.GREEN + screen_results['ATR'] + colorText.END
|
1166
|
+
|
1167
|
+
for column in columns_to_be_deleted:
|
1168
|
+
if column in save_results.columns:
|
1169
|
+
save_results.drop(column, axis=1, inplace=True, errors="ignore")
|
1170
|
+
screen_results.drop(column, axis=1, inplace=True, errors="ignore")
|
1171
|
+
|
1172
|
+
if "Stock" in screen_results.columns:
|
1173
|
+
screen_results.set_index("Stock", inplace=True)
|
1174
|
+
|
1175
|
+
if "Stock" in save_results.columns:
|
1176
|
+
save_results.set_index("Stock", inplace=True)
|
1177
|
+
|
1178
|
+
screen_results["volume"] = screen_results["volume"].astype(str)
|
1179
|
+
save_results["volume"] = save_results["volume"].astype(str)
|
1180
|
+
|
1181
|
+
screen_results.loc[:, "volume"] = screen_results.loc[:, "volume"].apply(
|
1182
|
+
lambda x: Utility.tools.formatRatio(float(ImageUtility.PKImageTools.removeAllColorStyles(x)), volume_ratio) if len(str(x).strip()) > 0 else ''
|
1183
|
+
)
|
1184
|
+
|
1185
|
+
save_results.loc[:, "volume"] = save_results.loc[:, "volume"].apply(
|
1186
|
+
lambda x: str(x) + "x"
|
1187
|
+
)
|
1188
|
+
|
1189
|
+
screen_results.rename(
|
1190
|
+
columns={
|
1191
|
+
"Trend": f"Trend({self.config_manager.daysToLookback}Prds)",
|
1192
|
+
"Breakout": f"Breakout({self.config_manager.daysToLookback}Prds)",
|
1193
|
+
},
|
1194
|
+
inplace=True,
|
1195
|
+
)
|
1196
|
+
|
1197
|
+
save_results.rename(
|
1198
|
+
columns={
|
1199
|
+
"Trend": f"Trend({self.config_manager.daysToLookback}Prds)",
|
1200
|
+
"Breakout": f"Breakout({self.config_manager.daysToLookback}Prds)",
|
1201
|
+
},
|
1202
|
+
inplace=True,
|
1203
|
+
)
|
1204
|
+
|
1205
|
+
except Exception as e:
|
1206
|
+
default_logger().debug(e, exc_info=True)
|
1207
|
+
|
1208
|
+
screen_results.dropna(how="all" if menu_option not in ["F"] else "any", axis=1, inplace=True)
|
1209
|
+
save_results.dropna(how="all" if menu_option not in ["F"] else "any", axis=1, inplace=True)
|
1210
|
+
|
1211
|
+
return screen_results, save_results
|
1212
|
+
|
1213
|
+
def print_notify_save_screened_results(self, screen_results, save_results, selected_choice,
|
1214
|
+
menu_choice_hierarchy, testing, user=None, execute_option=None, menu_option=None):
|
1215
|
+
"""
|
1216
|
+
Print, notify, and save screened results.
|
1217
|
+
|
1218
|
+
Args:
|
1219
|
+
screen_results: Screen results data
|
1220
|
+
save_results: Results to save
|
1221
|
+
selected_choice: Selected choice dictionary
|
1222
|
+
menu_choice_hierarchy: Menu choice hierarchy string
|
1223
|
+
testing (bool): Whether running in test mode
|
1224
|
+
user: User identifier
|
1225
|
+
execute_option: Execute option value
|
1226
|
+
menu_option: Menu option value
|
1227
|
+
"""
|
1228
|
+
# [Implementation of the complex result processing logic would go here]
|
1229
|
+
# This method handles the complete result presentation pipeline
|
1230
|
+
|
1231
|
+
pass
|
1232
|
+
|
1233
|
+
def remove_unknowns(self, screen_results, save_results):
|
1234
|
+
"""
|
1235
|
+
Remove unknown values from results data.
|
1236
|
+
|
1237
|
+
Args:
|
1238
|
+
screen_results: Screen results data
|
1239
|
+
save_results: Results to save
|
1240
|
+
|
1241
|
+
Returns:
|
1242
|
+
tuple: Cleaned screen results and save results
|
1243
|
+
"""
|
1244
|
+
for col in screen_results.keys():
|
1245
|
+
screen_results = screen_results[
|
1246
|
+
screen_results[col].astype(str).str.contains("Unknown") == False
|
1247
|
+
]
|
1248
|
+
|
1249
|
+
for col in save_results.keys():
|
1250
|
+
save_results = save_results[
|
1251
|
+
save_results[col].astype(str).str.contains("Unknown") == False
|
1252
|
+
]
|
1253
|
+
|
1254
|
+
return screen_results, save_results
|
1255
|
+
|
1256
|
+
def removed_unused_columns(self, screen_results, save_results, drop_additional_columns=[], user_args=None):
|
1257
|
+
"""
|
1258
|
+
Remove unused columns from results data.
|
1259
|
+
|
1260
|
+
Args:
|
1261
|
+
screen_results: Screen results data
|
1262
|
+
save_results: Results to save
|
1263
|
+
drop_additional_columns: Additional columns to drop
|
1264
|
+
user_args: User arguments
|
1265
|
+
|
1266
|
+
Returns:
|
1267
|
+
str: Summary returns string
|
1268
|
+
"""
|
1269
|
+
periods = self.config_manager.periodsRange
|
1270
|
+
|
1271
|
+
if user_args is not None and user_args.backtestdaysago is not None and int(user_args.backtestdaysago) < 22:
|
1272
|
+
drop_additional_columns.append("22-Pd")
|
1273
|
+
|
1274
|
+
summary_returns = "" # ("w.r.t. " + save_results["Date"].iloc[0]) if "Date" in save_results.columns else ""
|
1275
|
+
|
1276
|
+
for period in periods:
|
1277
|
+
if save_results is not None:
|
1278
|
+
with pd.option_context('mode.chained_assignment', None):
|
1279
|
+
save_results.drop(f"LTP{period}", axis=1, inplace=True, errors="ignore")
|
1280
|
+
save_results.drop(f"Growth{period}", axis=1, inplace=True, errors="ignore")
|
1281
|
+
|
1282
|
+
if len(drop_additional_columns) > 0:
|
1283
|
+
for col in drop_additional_columns:
|
1284
|
+
if col in save_results.columns:
|
1285
|
+
save_results.drop(col, axis=1, inplace=True, errors="ignore")
|
1286
|
+
|
1287
|
+
if screen_results is not None:
|
1288
|
+
with pd.option_context('mode.chained_assignment', None):
|
1289
|
+
screen_results.drop(f"LTP{period}", axis=1, inplace=True, errors="ignore")
|
1290
|
+
screen_results.drop(f"Growth{period}", axis=1, inplace=True, errors="ignore")
|
1291
|
+
|
1292
|
+
if len(drop_additional_columns) > 0:
|
1293
|
+
for col in drop_additional_columns:
|
1294
|
+
if col in screen_results.columns:
|
1295
|
+
screen_results.drop(col, axis=1, inplace=True, errors="ignore")
|
1296
|
+
|
1297
|
+
return summary_returns
|
1298
|
+
|
1299
|
+
def save_screen_results_encoded(self, encoded_text=None):
|
1300
|
+
"""
|
1301
|
+
Save screen results in encoded format.
|
1302
|
+
|
1303
|
+
Args:
|
1304
|
+
encoded_text: Encoded text to save
|
1305
|
+
|
1306
|
+
Returns:
|
1307
|
+
str: File name identifier
|
1308
|
+
"""
|
1309
|
+
import uuid
|
1310
|
+
uuid_file_name = str(uuid.uuid4())
|
1311
|
+
os.makedirs(os.path.dirname(os.path.join(Archiver.get_user_outputs_dir(), f"DeleteThis{os.sep}")), exist_ok=True)
|
1312
|
+
to_be_deleted_folder = os.path.join(Archiver.get_user_outputs_dir(), "DeleteThis")
|
1313
|
+
file_name = os.path.join(to_be_deleted_folder, uuid_file_name)
|
1314
|
+
|
1315
|
+
try:
|
1316
|
+
with open(file_name, 'w', encoding="utf-8") as f:
|
1317
|
+
f.write(encoded_text)
|
1318
|
+
except:
|
1319
|
+
pass
|
1320
|
+
|
1321
|
+
return f'{uuid_file_name}~{PKDateUtilities.currentDateTime().strftime("%Y-%m-%d %H:%M:%S.%f%z").replace(" ","~")}'
|
1322
|
+
|
1323
|
+
def read_screen_results_decoded(self, file_name=None):
|
1324
|
+
"""
|
1325
|
+
Read screen results from encoded format.
|
1326
|
+
|
1327
|
+
Args:
|
1328
|
+
file_name: File name to read
|
1329
|
+
|
1330
|
+
Returns:
|
1331
|
+
str: Decoded contents
|
1332
|
+
"""
|
1333
|
+
os.makedirs(os.path.dirname(os.path.join(Archiver.get_user_outputs_dir(), f"DeleteThis{os.sep}")), exist_ok=True)
|
1334
|
+
to_be_deleted_folder = os.path.join(Archiver.get_user_outputs_dir(), "DeleteThis")
|
1335
|
+
file_path = os.path.join(to_be_deleted_folder, file_name)
|
1336
|
+
contents = None
|
1337
|
+
|
1338
|
+
try:
|
1339
|
+
with open(file_path, 'r', encoding="utf-8") as f:
|
1340
|
+
contents = f.read()
|
1341
|
+
except:
|
1342
|
+
pass
|
1343
|
+
|
1344
|
+
return contents
|
1345
|
+
|
1346
|
+
|
1347
|
+
class TelegramNotifier:
|
1348
|
+
"""
|
1349
|
+
Handles all Telegram notifications and communications.
|
1350
|
+
Manages message sending, media attachments, and user subscriptions.
|
1351
|
+
"""
|
1352
|
+
|
1353
|
+
def __init__(self, dev_channel_id="-1001785195297"):
|
1354
|
+
"""
|
1355
|
+
Initialize the TelegramNotifier with development channel ID.
|
1356
|
+
|
1357
|
+
Args:
|
1358
|
+
dev_channel_id: Development channel ID for notifications
|
1359
|
+
"""
|
1360
|
+
self.DEV_CHANNEL_ID = dev_channel_id
|
1361
|
+
self.test_messages_queue = []
|
1362
|
+
self.media_group_dict = {}
|
1363
|
+
|
1364
|
+
def send_message_to_telegram_channel(self, message=None, photo_file_path=None,
|
1365
|
+
document_file_path=None, caption=None, user=None, mediagroup=False):
|
1366
|
+
"""
|
1367
|
+
Send message to Telegram channel with various attachment options.
|
1368
|
+
|
1369
|
+
Args:
|
1370
|
+
message: Text message to send
|
1371
|
+
photo_file_path: Path to photo file
|
1372
|
+
document_file_path: Path to document file
|
1373
|
+
caption: Caption for media
|
1374
|
+
user: User identifier
|
1375
|
+
mediagroup (bool): Whether sending media group
|
1376
|
+
"""
|
1377
|
+
default_logger().debug(f"Received message:{message}, caption:{caption}, for user : {user} with mediagroup:{mediagroup}")
|
1378
|
+
|
1379
|
+
if ("RUNNER" not in os.environ.keys() and (self.user_passed_args is not None and not self.user_passed_args.log)) or (self.user_passed_args is not None and self.user_passed_args.telegram):
|
1380
|
+
return
|
1381
|
+
|
1382
|
+
if user is None and self.user_passed_args is not None and self.user_passed_args.user is not None:
|
1383
|
+
user = self.user_passed_args.user
|
1384
|
+
|
1385
|
+
if not mediagroup:
|
1386
|
+
if self.test_messages_queue is not None:
|
1387
|
+
self.test_messages_queue.append(f"message:{message}\ncaption:{caption}\nuser:{user}\ndocument:{document_file_path}")
|
1388
|
+
|
1389
|
+
if len(self.test_messages_queue) > 10:
|
1390
|
+
self.test_messages_queue.pop(0)
|
1391
|
+
|
1392
|
+
if user is not None and caption is not None:
|
1393
|
+
caption = f"{caption.replace('&','n')}."
|
1394
|
+
|
1395
|
+
if message is not None:
|
1396
|
+
try:
|
1397
|
+
message = message.replace("&", "n").replace("<", "*")
|
1398
|
+
send_message(message, userID=user)
|
1399
|
+
except Exception as e:
|
1400
|
+
default_logger().debug(e, exc_info=True)
|
1401
|
+
else:
|
1402
|
+
message = ""
|
1403
|
+
|
1404
|
+
if photo_file_path is not None:
|
1405
|
+
try:
|
1406
|
+
if caption is not None:
|
1407
|
+
caption = f"{caption.replace('&','n')}"
|
1408
|
+
send_photo(photo_file_path, (caption if len(caption) <= 1024 else ""), userID=user)
|
1409
|
+
sleep(2)
|
1410
|
+
except Exception as e:
|
1411
|
+
default_logger().debug(e, exc_info=True)
|
1412
|
+
|
1413
|
+
if document_file_path is not None:
|
1414
|
+
try:
|
1415
|
+
if caption is not None and isinstance(caption, str):
|
1416
|
+
caption = f"{caption.replace('&','n')}"
|
1417
|
+
send_document(document_file_path, (caption if len(caption) <= 1024 else ""), userID=user)
|
1418
|
+
sleep(2)
|
1419
|
+
except Exception as e:
|
1420
|
+
default_logger().debug(e, exc_info=True)
|
1421
|
+
else:
|
1422
|
+
file_paths = []
|
1423
|
+
file_captions = []
|
1424
|
+
|
1425
|
+
if "ATTACHMENTS" in self.media_group_dict.keys():
|
1426
|
+
attachments = self.media_group_dict["ATTACHMENTS"]
|
1427
|
+
num_files = len(attachments)
|
1428
|
+
|
1429
|
+
if num_files >= 4:
|
1430
|
+
self.media_group_dict["ATTACHMENTS"] = []
|
1431
|
+
|
1432
|
+
for attachment in attachments:
|
1433
|
+
file_paths.append(attachment["FILEPATH"])
|
1434
|
+
clean_caption = attachment["CAPTION"].replace('&', 'n')[:1024]
|
1435
|
+
|
1436
|
+
if "<pre>" in clean_caption and "</pre>" not in clean_caption:
|
1437
|
+
clean_caption = f"{clean_caption[:1018]}</pre>"
|
1438
|
+
|
1439
|
+
file_captions.append(clean_caption)
|
1440
|
+
|
1441
|
+
if self.test_messages_queue is not None:
|
1442
|
+
self.test_messages_queue.append(f"message:{file_captions[-1]}\ncaption:{file_captions[-1]}\nuser:{user}\ndocument:{file_paths[-1]}")
|
1443
|
+
|
1444
|
+
if len(self.test_messages_queue) > 10:
|
1445
|
+
self.test_messages_queue.pop(0)
|
1446
|
+
|
1447
|
+
if len(file_paths) > 0 and not self.user_passed_args.monitor:
|
1448
|
+
resp = send_media_group(user=self.user_passed_args.user,
|
1449
|
+
png_paths=[],
|
1450
|
+
png_album_caption=None,
|
1451
|
+
file_paths=file_paths,
|
1452
|
+
file_captions=file_captions)
|
1453
|
+
|
1454
|
+
if resp is not None:
|
1455
|
+
default_logger().debug(resp.text, exc_info=True)
|
1456
|
+
|
1457
|
+
caption = f"{str(len(file_captions))} files sent!"
|
1458
|
+
message = self.media_group_dict["CAPTION"].replace('&', 'n').replace("<", "*")[:1024] if "CAPTION" in self.media_group_dict.keys() else "-"
|
1459
|
+
default_logger().debug(f"Received updated message:{message}, caption:{caption}, for user : {user} with mediagroup:{mediagroup}")
|
1460
|
+
else:
|
1461
|
+
default_logger().debug(f"No ATTACHMENTS in media_group_dict: {self.media_group_dict.keys(),}\nReceived updated message:{message}, caption:{caption}, for user : {user} with mediagroup:{mediagroup}")
|
1462
|
+
|
1463
|
+
for f in file_paths:
|
1464
|
+
try:
|
1465
|
+
if "RUNNER" in os.environ.keys():
|
1466
|
+
os.remove(f)
|
1467
|
+
elif not f.endswith("xlsx"):
|
1468
|
+
os.remove(f)
|
1469
|
+
except:
|
1470
|
+
pass
|
1471
|
+
|
1472
|
+
self.handle_alert_subscriptions(user, message)
|
1473
|
+
|
1474
|
+
if user is not None:
|
1475
|
+
if str(user) != str(self.DEV_CHANNEL_ID) and self.user_passed_args is not None and not self.user_passed_args.monitor:
|
1476
|
+
send_message(
|
1477
|
+
f"Responded back to userId:{user} with {caption}.{message} [{self.user_passed_args.options.replace(':D','')}]",
|
1478
|
+
userID=self.DEV_CHANNEL_ID,
|
1479
|
+
)
|
1480
|
+
|
1481
|
+
def handle_alert_subscriptions(self, user, message):
|
1482
|
+
"""
|
1483
|
+
Handle user subscriptions to automated alerts.
|
1484
|
+
|
1485
|
+
Args:
|
1486
|
+
user: User identifier
|
1487
|
+
message: Message content
|
1488
|
+
"""
|
1489
|
+
if user is not None and message is not None and "|" in str(message):
|
1490
|
+
if int(user) > 0:
|
1491
|
+
scan_id = message.split("|")[0].replace("*b>", "").strip()
|
1492
|
+
from PKDevTools.classes.DBManager import DBManager
|
1493
|
+
db_manager = DBManager()
|
1494
|
+
|
1495
|
+
if db_manager.url is not None and db_manager.token is not None:
|
1496
|
+
alert_user = db_manager.alertsForUser(int(user))
|
1497
|
+
|
1498
|
+
if alert_user is None or len(alert_user.scannerJobs) == 0 or str(scan_id) not in alert_user.scannerJobs:
|
1499
|
+
reply_markup = {
|
1500
|
+
"inline_keyboard": [
|
1501
|
+
[{"text": f"Yes! Subscribe", "callback_data": f"SUB_{scan_id}"}]
|
1502
|
+
],
|
1503
|
+
}
|
1504
|
+
|
1505
|
+
send_message(message=f"🔴 <b>Please check your current alerts, balance and subscriptions using /OTP before subscribing for alerts</b>.🔴 If you are not already subscribed to this alert, would you like to subscribe to this ({scan_id}) automated scan alert for a day during market hours (NSE - IST timezone)? You will need to pay ₹ {'40' if str(scan_id).upper().startswith('P') else '31'} (One time) for automated alerts to {scan_id} all day on the day of subscription. 🔴 If you say <b>Yes</b>, the corresponding charges will be deducted from your alerts balance!🔴",
|
1506
|
+
userID=int(user),
|
1507
|
+
reply_markup=reply_markup)
|
1508
|
+
elif alert_user is not None and len(alert_user.scannerJobs) > 0 and str(scan_id) in alert_user.scannerJobs:
|
1509
|
+
send_message(message=f"Thank you for subscribing to (<b>{scan_id}</b>) automated scan alert! We truly hope you are enjoying the alerts! You will continue to receive alerts for the duration of NSE Market hours for today. For any feedback, drop a note to @ItsOnlyPK.",
|
1510
|
+
userID=int(user),)
|
1511
|
+
|
1512
|
+
def send_test_status(self, screen_results, label, user=None):
|
1513
|
+
"""
|
1514
|
+
Send test status message to Telegram.
|
1515
|
+
|
1516
|
+
Args:
|
1517
|
+
screen_results: Screen results data
|
1518
|
+
label: Test label
|
1519
|
+
user: User identifier
|
1520
|
+
"""
|
1521
|
+
msg = "<b>SUCCESS</b>" if (screen_results is not None and len(screen_results) >= 1) else "<b>FAIL</b>"
|
1522
|
+
self.send_message_to_telegram_channel(
|
1523
|
+
message=f"{msg}: Found {len(screen_results) if screen_results is not None else 0} Stocks for {label}", user=user
|
1524
|
+
)
|
1525
|
+
|
1526
|
+
def send_quick_scan_result(self, menu_choice_hierarchy, user, tabulated_results,
|
1527
|
+
markdown_results, caption, png_name, png_extension,
|
1528
|
+
addendum=None, addendum_label=None, backtest_summary="",
|
1529
|
+
backtest_detail="", summary_label=None, detail_label=None,
|
1530
|
+
legend_prefix_text="", force_send=False):
|
1531
|
+
"""
|
1532
|
+
Send quick scan result to Telegram with formatted output.
|
1533
|
+
|
1534
|
+
Args:
|
1535
|
+
menu_choice_hierarchy: Menu choice hierarchy string
|
1536
|
+
user: User identifier
|
1537
|
+
tabulated_results: Tabulated results text
|
1538
|
+
markdown_results: Markdown formatted results
|
1539
|
+
caption: Caption for the message
|
1540
|
+
png_name: PNG file name
|
1541
|
+
png_extension: PNG file extension
|
1542
|
+
addendum: Additional content
|
1543
|
+
addendum_label: Label for addendum
|
1544
|
+
backtest_summary: Backtest summary
|
1545
|
+
backtest_detail: Backtest detail
|
1546
|
+
summary_label: Label for summary
|
1547
|
+
detail_label: Label for detail
|
1548
|
+
legend_prefix_text: Legend prefix text
|
1549
|
+
force_send (bool): Whether to force send
|
1550
|
+
"""
|
1551
|
+
if "PKDevTools_Default_Log_Level" not in os.environ.keys():
|
1552
|
+
if (("RUNNER" not in os.environ.keys()) or ("RUNNER" in os.environ.keys() and os.environ["RUNNER"] == "LOCAL_RUN_SCANNER")):
|
1553
|
+
return
|
1554
|
+
|
1555
|
+
try:
|
1556
|
+
if not is_token_telegram_configured():
|
1557
|
+
return
|
1558
|
+
|
1559
|
+
ImageUtility.PKImageTools.tableToImage(
|
1560
|
+
markdown_results,
|
1561
|
+
tabulated_results,
|
1562
|
+
png_name + png_extension,
|
1563
|
+
menu_choice_hierarchy,
|
1564
|
+
backtestSummary=backtest_summary,
|
1565
|
+
backtestDetail=backtest_detail,
|
1566
|
+
addendum=addendum,
|
1567
|
+
addendumLabel=addendum_label,
|
1568
|
+
summaryLabel=summary_label,
|
1569
|
+
detailLabel=detail_label,
|
1570
|
+
legendPrefixText=legend_prefix_text
|
1571
|
+
)
|
1572
|
+
|
1573
|
+
if force_send:
|
1574
|
+
self.send_message_to_telegram_channel(
|
1575
|
+
message=None,
|
1576
|
+
document_filePath=png_name + png_extension,
|
1577
|
+
caption=caption,
|
1578
|
+
user=user,
|
1579
|
+
)
|
1580
|
+
os.remove(png_name + png_extension)
|
1581
|
+
except Exception as e:
|
1582
|
+
default_logger().debug(e, exc_info=True)
|
1583
|
+
pass
|
1584
|
+
|
1585
|
+
|
1586
|
+
class DataManager:
|
1587
|
+
"""
|
1588
|
+
Manages data loading, caching, and storage operations.
|
1589
|
+
Handles database interactions, file operations, and data retrieval.
|
1590
|
+
"""
|
1591
|
+
|
1592
|
+
def __init__(self, config_manager, user_passed_args):
|
1593
|
+
"""
|
1594
|
+
Initialize the DataManager with configuration and user arguments.
|
1595
|
+
|
1596
|
+
Args:
|
1597
|
+
config_manager: Configuration manager instance
|
1598
|
+
user_passed_args: User passed arguments
|
1599
|
+
"""
|
1600
|
+
self.config_manager = config_manager
|
1601
|
+
self.user_passed_args = user_passed_args
|
1602
|
+
self.fetcher = Fetcher.screenerStockDataFetcher(self.config_manager)
|
1603
|
+
self.mstar_fetcher = morningstarDataFetcher(self.config_manager)
|
1604
|
+
self.stock_dict_primary = None
|
1605
|
+
self.stock_dict_secondary = None
|
1606
|
+
self.load_count = 0
|
1607
|
+
self.loaded_stock_data = False
|
1608
|
+
self.list_stock_codes = None
|
1609
|
+
self.last_scan_output_stock_codes = None
|
1610
|
+
self.run_clean_up = False
|
1611
|
+
|
1612
|
+
@exit_after(10)
|
1613
|
+
def try_load_data_on_background_thread(self):
|
1614
|
+
"""Attempt to load data on a background thread with timeout."""
|
1615
|
+
if self.stock_dict_primary is None:
|
1616
|
+
self.stock_dict_primary = {}
|
1617
|
+
self.stock_dict_secondary = {}
|
1618
|
+
self.loaded_stock_data = False
|
1619
|
+
|
1620
|
+
self.config_manager.getConfig(ConfigManager.parser)
|
1621
|
+
self.default_answer = "Y"
|
1622
|
+
self.user_passed_args = None
|
1623
|
+
|
1624
|
+
with SuppressOutput(suppress_stderr=True, suppress_stdout=True):
|
1625
|
+
list_stock_codes = self.fetcher.fetchStockCodes(int(self.config_manager.defaultIndex), stockCode=None)
|
1626
|
+
|
1627
|
+
self.load_database_or_fetch(downloadOnly=True, list_stock_codes=list_stock_codes,
|
1628
|
+
menu_option="X", index_option=int(self.config_manager.defaultIndex))
|
1629
|
+
|
1630
|
+
def load_database_or_fetch(self, download_only, list_stock_codes, menu_option, index_option):
|
1631
|
+
"""
|
1632
|
+
Load data from database or fetch from source.
|
1633
|
+
|
1634
|
+
Args:
|
1635
|
+
download_only (bool): Whether only downloading data
|
1636
|
+
list_stock_codes: List of stock codes to process
|
1637
|
+
menu_option: Selected menu option
|
1638
|
+
index_option: Selected index option
|
1639
|
+
|
1640
|
+
Returns:
|
1641
|
+
tuple: Primary and secondary stock data dictionaries
|
1642
|
+
"""
|
1643
|
+
if menu_option not in ["C"]:
|
1644
|
+
self.stock_dict_primary = AssetsManager.PKAssetsManager.loadStockData(
|
1645
|
+
self.stock_dict_primary,
|
1646
|
+
self.config_manager,
|
1647
|
+
downloadOnly=download_only,
|
1648
|
+
defaultAnswer=self.default_answer,
|
1649
|
+
forceLoad=(menu_option in ["X", "B", "G", "S", "F"]),
|
1650
|
+
stockCodes=list_stock_codes,
|
1651
|
+
exchangeSuffix="" if (index_option == 15 or (self.config_manager.defaultIndex == 15 and index_option == 0)) else ".NS",
|
1652
|
+
userDownloadOption=menu_option
|
1653
|
+
)
|
1654
|
+
|
1655
|
+
if menu_option not in ["C"] and (self.user_passed_args is not None and
|
1656
|
+
(self.user_passed_args.monitor is not None or \
|
1657
|
+
("|" in self.user_passed_args.options and ':i' in self.user_passed_args.options) or \
|
1658
|
+
(":33:3:" in self.user_passed_args.options or \
|
1659
|
+
":32:" in self.user_passed_args.options or \
|
1660
|
+
":38:" in self.user_passed_args.options))):
|
1661
|
+
|
1662
|
+
prev_duration = self.config_manager.duration
|
1663
|
+
prev_period = self.config_manager.period
|
1664
|
+
candle_duration = (self.user_passed_args.intraday if (self.user_passed_args is not None and self.user_passed_args.intraday is not None) else ("1m" if self.config_manager.duration.endswith("d") else self.config_manager.duration))
|
1665
|
+
|
1666
|
+
self.config_manager.toggleConfig(candleDuration=candle_duration, clearCache=False)
|
1667
|
+
|
1668
|
+
if self.user_passed_args is not None and ":33:3:" in self.user_passed_args.options:
|
1669
|
+
exists, cache_file = AssetsManager.PKAssetsManager.afterMarketStockDataExists(True, forceLoad=(menu_option in ["X", "B", "G", "S", "F"]))
|
1670
|
+
cache_file = os.path.join(Archiver.get_user_data_dir(), cache_file)
|
1671
|
+
cache_file_size = os.stat(cache_file).st_size if os.path.exists(cache_file) else 0
|
1672
|
+
|
1673
|
+
if cache_file_size < 1024 * 1024 * 100: # 1m data for 5d is at least 450MB
|
1674
|
+
self.config_manager.deleteFileWithPattern(pattern="*intraday_stock_data_*.pkl", rootDir=Archiver.get_user_data_dir())
|
1675
|
+
|
1676
|
+
self.config_manager.duration = "1m"
|
1677
|
+
self.config_manager.period = "5d"
|
1678
|
+
self.config_manager.setConfig(ConfigManager.parser, default=True, showFileCreatedText=False)
|
1679
|
+
|
1680
|
+
self.stock_dict_secondary = AssetsManager.PKAssetsManager.loadStockData(
|
1681
|
+
self.stock_dict_secondary,
|
1682
|
+
self.config_manager,
|
1683
|
+
downloadOnly=download_only,
|
1684
|
+
defaultAnswer=self.default_answer,
|
1685
|
+
forceLoad=(menu_option in ["X", "B", "G", "S", "F"]),
|
1686
|
+
stockCodes=list_stock_codes,
|
1687
|
+
isIntraday=True,
|
1688
|
+
exchangeSuffix="" if (index_option == 15 or (self.config_manager.defaultIndex == 15 and index_option == 0)) else ".NS",
|
1689
|
+
userDownloadOption=menu_option
|
1690
|
+
)
|
1691
|
+
|
1692
|
+
self.config_manager.duration = prev_duration
|
1693
|
+
self.config_manager.period = prev_period
|
1694
|
+
self.config_manager.setConfig(ConfigManager.parser, default=True, showFileCreatedText=False)
|
1695
|
+
|
1696
|
+
self.loaded_stock_data = True
|
1697
|
+
Utility.tools.loadLargeDeals()
|
1698
|
+
|
1699
|
+
return self.stock_dict_primary, self.stock_dict_secondary
|
1700
|
+
|
1701
|
+
def get_latest_trade_date_time(self, stock_dict_primary):
|
1702
|
+
"""
|
1703
|
+
Get the latest trade date and time from stock data.
|
1704
|
+
|
1705
|
+
Args:
|
1706
|
+
stock_dict_primary: Primary stock data dictionary
|
1707
|
+
|
1708
|
+
Returns:
|
1709
|
+
tuple: Last trade date and time strings
|
1710
|
+
"""
|
1711
|
+
stocks = list(stock_dict_primary.keys())
|
1712
|
+
stock = stocks[0]
|
1713
|
+
|
1714
|
+
try:
|
1715
|
+
last_trade_date = PKDateUtilities.currentDateTime().strftime("%Y-%m-%d")
|
1716
|
+
last_trade_time_ist = PKDateUtilities.currentDateTime().strftime("%H:%M:%S")
|
1717
|
+
df = pd.DataFrame(data=stock_dict_primary[stock]["data"],
|
1718
|
+
columns=stock_dict_primary[stock]["columns"],
|
1719
|
+
index=stock_dict_primary[stock]["index"])
|
1720
|
+
ts = df.index[-1]
|
1721
|
+
last_traded = pd.to_datetime(ts, unit='s', utc=True)
|
1722
|
+
last_trade_date = last_traded.strftime("%Y-%m-%d")
|
1723
|
+
last_trade_time = last_traded.strftime("%H:%M:%S")
|
1724
|
+
|
1725
|
+
if last_trade_time == "00:00:00":
|
1726
|
+
last_trade_time = last_trade_time_ist
|
1727
|
+
except:
|
1728
|
+
pass
|
1729
|
+
|
1730
|
+
return last_trade_date, last_trade_time
|
1731
|
+
|
1732
|
+
def prepare_stocks_for_screening(self, testing, download_only, list_stock_codes, index_option):
|
1733
|
+
"""
|
1734
|
+
Prepare stocks for screening operations.
|
1735
|
+
|
1736
|
+
Args:
|
1737
|
+
testing (bool): Whether running in test mode
|
1738
|
+
download_only (bool): Whether only downloading data
|
1739
|
+
list_stock_codes: List of stock codes to process
|
1740
|
+
index_option: Selected index option
|
1741
|
+
|
1742
|
+
Returns:
|
1743
|
+
list: Prepared list of stock codes
|
1744
|
+
"""
|
1745
|
+
if not download_only:
|
1746
|
+
self.update_menu_choice_hierarchy()
|
1747
|
+
|
1748
|
+
index_option = int(index_option)
|
1749
|
+
|
1750
|
+
if list_stock_codes is None or len(list_stock_codes) == 0:
|
1751
|
+
if index_option >= 0 and index_option <= 14:
|
1752
|
+
should_suppress = not OutputControls().enableMultipleLineOutput
|
1753
|
+
|
1754
|
+
with SuppressOutput(suppress_stderr=should_suppress, suppress_stdout=should_suppress):
|
1755
|
+
list_stock_codes = self.fetcher.fetchStockCodes(
|
1756
|
+
index_option, stockCode=None
|
1757
|
+
)
|
1758
|
+
elif index_option == 15:
|
1759
|
+
OutputControls().printOutput(" [+] Getting Stock Codes From NASDAQ... ", end="")
|
1760
|
+
nasdaq = PKNasdaqIndexFetcher(self.config_manager)
|
1761
|
+
list_stock_codes, _ = nasdaq.fetchNasdaqIndexConstituents()
|
1762
|
+
|
1763
|
+
if len(list_stock_codes) > 10:
|
1764
|
+
OutputControls().printOutput(
|
1765
|
+
colorText.GREEN
|
1766
|
+
+ ("=> Done! Fetched %d stock codes." % len(list_stock_codes))
|
1767
|
+
+ colorText.END
|
1768
|
+
)
|
1769
|
+
|
1770
|
+
if self.config_manager.shuffleEnabled:
|
1771
|
+
random.shuffle(list_stock_codes)
|
1772
|
+
OutputControls().printOutput(
|
1773
|
+
colorText.BLUE
|
1774
|
+
+ " [+] Stock shuffling is active."
|
1775
|
+
+ colorText.END
|
1776
|
+
)
|
1777
|
+
else:
|
1778
|
+
OutputControls().printOutput(
|
1779
|
+
colorText.FAIL
|
1780
|
+
+ ("=> Failed! Could not fetch stock codes from NASDAQ!")
|
1781
|
+
+ colorText.END
|
1782
|
+
)
|
1783
|
+
|
1784
|
+
if (list_stock_codes is None or len(list_stock_codes) == 0) and testing:
|
1785
|
+
list_stock_codes = [TEST_STKCODE if index_option < 15 else "AMD"]
|
1786
|
+
|
1787
|
+
if index_option == 0:
|
1788
|
+
self.selected_choice["3"] = ".".join(list_stock_codes)
|
1789
|
+
|
1790
|
+
if testing:
|
1791
|
+
list_stock_codes = [random.choice(list_stock_codes)]
|
1792
|
+
|
1793
|
+
return list_stock_codes
|
1794
|
+
|
1795
|
+
@Halo(text='', spinner='dots')
|
1796
|
+
def get_performance_stats(self):
|
1797
|
+
"""Get performance statistics from Morningstar."""
|
1798
|
+
return self.mstar_fetcher.fetchMorningstarStocksPerformanceForExchange()
|
1799
|
+
|
1800
|
+
@Halo(text='', spinner='dots')
|
1801
|
+
def get_mfi_stats(self, pop_option):
|
1802
|
+
"""
|
1803
|
+
Get MFI (Money Flow Index) statistics.
|
1804
|
+
|
1805
|
+
Args:
|
1806
|
+
pop_option: Population option
|
1807
|
+
|
1808
|
+
Returns:
|
1809
|
+
DataFrame: MFI statistics results
|
1810
|
+
"""
|
1811
|
+
if pop_option == 4:
|
1812
|
+
screen_results = self.mstar_fetcher.fetchMorningstarTopDividendsYieldStocks()
|
1813
|
+
elif pop_option in [1, 2]:
|
1814
|
+
screen_results = self.mstar_fetcher.fetchMorningstarFundFavouriteStocks(
|
1815
|
+
"NoOfFunds" if pop_option == 2 else "ChangeInShares"
|
1816
|
+
)
|
1817
|
+
|
1818
|
+
return screen_results
|
1819
|
+
|
1820
|
+
def handle_request_for_specific_stocks(self, options, index_option):
|
1821
|
+
"""
|
1822
|
+
Handle request for specific stocks based on options.
|
1823
|
+
|
1824
|
+
Args:
|
1825
|
+
options: Options list
|
1826
|
+
index_option: Selected index option
|
1827
|
+
|
1828
|
+
Returns:
|
1829
|
+
list: List of specific stock codes
|
1830
|
+
"""
|
1831
|
+
list_stock_codes = [] if self.list_stock_codes is None or len(self.list_stock_codes) == 0 else self.list_stock_codes
|
1832
|
+
str_options = ""
|
1833
|
+
|
1834
|
+
if isinstance(options, list):
|
1835
|
+
str_options = ":".join(options).split(">")[0]
|
1836
|
+
else:
|
1837
|
+
str_options = options.split(">")[0]
|
1838
|
+
|
1839
|
+
if index_option == 0:
|
1840
|
+
if len(str_options) >= 4:
|
1841
|
+
str_options = str_options.replace(":D:", ":").replace(">", "")
|
1842
|
+
provided_options = str_options.split(":")
|
1843
|
+
|
1844
|
+
for option in provided_options:
|
1845
|
+
if not "".join(str(option).split(".")).isdecimal() and len(option.strip()) > 1:
|
1846
|
+
list_stock_codes = str(option.strip()).split(",")
|
1847
|
+
break
|
1848
|
+
|
1849
|
+
return list_stock_codes
|
1850
|
+
|
1851
|
+
def cleanup_local_results(self):
|
1852
|
+
"""Clean up local results and temporary files."""
|
1853
|
+
# global run_clean_up
|
1854
|
+
self.run_clean_up = True
|
1855
|
+
|
1856
|
+
if self.user_passed_args.answerdefault is not None or self.user_passed_args.systemlaunched or self.user_passed_args.testbuild:
|
1857
|
+
return
|
1858
|
+
|
1859
|
+
from PKDevTools.classes.NSEMarketStatus import NSEMarketStatus
|
1860
|
+
|
1861
|
+
if not NSEMarketStatus().shouldFetchNextBell()[0]:
|
1862
|
+
return
|
1863
|
+
|
1864
|
+
launcher = f'"{sys.argv[0]}"' if " " in sys.argv[0] else sys.argv[0]
|
1865
|
+
should_prompt = (launcher.endswith(".py\"") or launcher.endswith(".py")) and (self.user_passed_args is None or self.user_passed_args.answerdefault is None)
|
1866
|
+
response = "N"
|
1867
|
+
|
1868
|
+
if should_prompt:
|
1869
|
+
response = input(f" [+] {colorText.WARN}Clean up local non-essential system generated data?{colorText.END}{colorText.FAIL}[Default: {response}]{colorText.END}\n (User generated reports won't be deleted.) :") or response
|
1870
|
+
|
1871
|
+
if "y" in response.lower():
|
1872
|
+
dirs = [Archiver.get_user_data_dir(), Archiver.get_user_cookies_dir(),
|
1873
|
+
Archiver.get_user_temp_dir(), Archiver.get_user_indices_dir()]
|
1874
|
+
|
1875
|
+
for dir in dirs:
|
1876
|
+
self.config_manager.deleteFileWithPattern(rootDir=dir, pattern="*")
|
1877
|
+
|
1878
|
+
response = input(f"\n [+] {colorText.WARN}Clean up local user generated reports as well?{colorText.END} {colorText.FAIL}[Default: N]{colorText.END} :") or "n"
|
1879
|
+
|
1880
|
+
if "y" in response.lower():
|
1881
|
+
self.config_manager.deleteFileWithPattern(rootDir=Archiver.get_user_reports_dir(), pattern="*.*")
|
1882
|
+
|
1883
|
+
ConsoleUtility.PKConsoleTools.clearScreen(forceTop=True)
|
1884
|
+
|
1885
|
+
|
1886
|
+
class BacktestManager:
|
1887
|
+
"""
|
1888
|
+
Manages backtesting operations and analysis.
|
1889
|
+
Handles backtest execution, result processing, and performance reporting.
|
1890
|
+
"""
|
1891
|
+
|
1892
|
+
def __init__(self, config_manager, user_passed_args):
|
1893
|
+
"""
|
1894
|
+
Initialize the BacktestManager with configuration and user arguments.
|
1895
|
+
|
1896
|
+
Args:
|
1897
|
+
config_manager: Configuration manager instance
|
1898
|
+
user_passed_args: User passed arguments
|
1899
|
+
"""
|
1900
|
+
self.config_manager = config_manager
|
1901
|
+
self.user_passed_args = user_passed_args
|
1902
|
+
|
1903
|
+
def take_backtest_inputs(self, menu_option=None, index_option=None, execute_option=None, backtest_period=0):
|
1904
|
+
"""
|
1905
|
+
Take backtest inputs from user.
|
1906
|
+
|
1907
|
+
Args:
|
1908
|
+
menu_option: Selected menu option
|
1909
|
+
index_option: Selected index option
|
1910
|
+
execute_option: Selected execute option
|
1911
|
+
backtest_period: Backtest period in days
|
1912
|
+
|
1913
|
+
Returns:
|
1914
|
+
tuple: Index option, execute option, and backtest period
|
1915
|
+
"""
|
1916
|
+
g10k = '"Growth of 10k"'
|
1917
|
+
OutputControls().printOutput(
|
1918
|
+
colorText.GREEN
|
1919
|
+
+ f" [+] For {g10k if menu_option == 'G' else 'backtesting'}, you can choose from (1,2,3,4,5,10,15,22,30) or any other custom periods (< 1y)."
|
1920
|
+
)
|
1921
|
+
|
1922
|
+
try:
|
1923
|
+
if backtest_period == 0:
|
1924
|
+
backtest_period = int(
|
1925
|
+
input(
|
1926
|
+
colorText.FAIL
|
1927
|
+
+ f" [+] Enter {g10k if menu_option == 'G' else 'backtesting'} period (Default is {15 if menu_option == 'G' else 30} [days]): "
|
1928
|
+
)
|
1929
|
+
)
|
1930
|
+
except Exception as e:
|
1931
|
+
default_logger().debug(e, exc_info=True)
|
1932
|
+
|
1933
|
+
if backtest_period == 0:
|
1934
|
+
backtest_period = 3 if menu_option == "G" else 30
|
1935
|
+
|
1936
|
+
index_option, execute_option = self.init_post_level0_execution(
|
1937
|
+
menu_option=menu_option,
|
1938
|
+
index_option=index_option,
|
1939
|
+
execute_option=execute_option,
|
1940
|
+
skip=["N", "E"],
|
1941
|
+
)
|
1942
|
+
|
1943
|
+
index_option, execute_option = self.init_post_level1_execution(
|
1944
|
+
index_option=index_option,
|
1945
|
+
execute_option=execute_option,
|
1946
|
+
skip=[
|
1947
|
+
"0",
|
1948
|
+
"29",
|
1949
|
+
"42",
|
1950
|
+
],
|
1951
|
+
)
|
1952
|
+
|
1953
|
+
return index_option, execute_option, backtest_period
|
1954
|
+
|
1955
|
+
@Halo(text='', spinner='dots')
|
1956
|
+
def prepare_grouped_x_ray(self, backtest_period, backtest_df):
|
1957
|
+
"""
|
1958
|
+
Prepare grouped X-ray analysis for backtest results.
|
1959
|
+
|
1960
|
+
Args:
|
1961
|
+
backtest_period: Backtest period in days
|
1962
|
+
backtest_df: Backtest dataframe
|
1963
|
+
|
1964
|
+
Returns:
|
1965
|
+
DataFrame: Grouped X-ray analysis results
|
1966
|
+
"""
|
1967
|
+
df_grouped = backtest_df.groupby("Date")
|
1968
|
+
self.user_passed_args.backtestdaysago = backtest_period
|
1969
|
+
df_xray = None
|
1970
|
+
group_counter = 0
|
1971
|
+
tasks_list = []
|
1972
|
+
|
1973
|
+
for calc_for_date, df_group in df_grouped:
|
1974
|
+
group_counter += 1
|
1975
|
+
func_args = (df_group, self.user_passed_args, calc_for_date, f"Portfolio X-Ray | {calc_for_date} | {group_counter} of {len(df_grouped)}")
|
1976
|
+
task = PKTask(f"Portfolio X-Ray | {calc_for_date} | {group_counter} of {len(df_grouped)}",
|
1977
|
+
long_running_fn=PortfolioXRay.performXRay,
|
1978
|
+
long_running_fn_args=func_args)
|
1979
|
+
task.total = len(df_grouped)
|
1980
|
+
tasks_list.append(task)
|
1981
|
+
|
1982
|
+
try:
|
1983
|
+
if 'RUNNER' not in os.environ.keys():
|
1984
|
+
PKScheduler.scheduleTasks(tasks_list, f"Portfolio X-Ray for ({len(df_grouped)})", showProgressBars=False, timeout=600)
|
1985
|
+
else:
|
1986
|
+
for task in tasks_list:
|
1987
|
+
task.long_running_fn(*(task,))
|
1988
|
+
|
1989
|
+
for task in tasks_list:
|
1990
|
+
p_df = task.result
|
1991
|
+
|
1992
|
+
if p_df is not None:
|
1993
|
+
if df_xray is not None:
|
1994
|
+
df_xray = pd.concat([df_xray, p_df.copy()], axis=0)
|
1995
|
+
else:
|
1996
|
+
df_xray = p_df.copy()
|
1997
|
+
|
1998
|
+
self.removed_unused_columns(None, backtest_df, ["Consol.", "Breakout", "RSI", "Pattern", "CCI"], userArgs=self.user_passed_args)
|
1999
|
+
df_xray = df_xray.replace(np.nan, "", regex=True)
|
2000
|
+
df_xray = PortfolioXRay.xRaySummary(df_xray)
|
2001
|
+
df_xray.loc[:, "Date"] = df_xray.loc[:, "Date"].apply(
|
2002
|
+
lambda x: x.replace("-", "/")
|
2003
|
+
)
|
2004
|
+
except Exception as e:
|
2005
|
+
default_logger().debug(e, exc_info=True)
|
2006
|
+
pass
|
2007
|
+
|
2008
|
+
return df_xray
|
2009
|
+
|
2010
|
+
def finish_backtest_data_cleanup(self, backtest_df, df_xray):
|
2011
|
+
"""
|
2012
|
+
Finish backtest data cleanup and preparation.
|
2013
|
+
|
2014
|
+
Args:
|
2015
|
+
backtest_df: Backtest dataframe
|
2016
|
+
df_xray: X-ray analysis dataframe
|
2017
|
+
|
2018
|
+
Returns:
|
2019
|
+
tuple: Summary dataframe, sorting flag, and sort keys
|
2020
|
+
"""
|
2021
|
+
if df_xray is not None and len(df_xray) > 10:
|
2022
|
+
self.show_backtest_results(df_xray, sortKey="Date", optionalName="Insights")
|
2023
|
+
|
2024
|
+
summary_df = backtestSummary(backtest_df)
|
2025
|
+
backtest_df.loc[:, "Date"] = backtest_df.loc[:, "Date"].apply(
|
2026
|
+
lambda x: x.replace("-", "/")
|
2027
|
+
)
|
2028
|
+
|
2029
|
+
self.show_backtest_results(backtest_df)
|
2030
|
+
self.show_backtest_results(summary_df, optionalName="Summary")
|
2031
|
+
|
2032
|
+
sorting = False if self.default_answer is not None else True
|
2033
|
+
tasks_list = []
|
2034
|
+
sort_keys = {
|
2035
|
+
"S": "Stock",
|
2036
|
+
"D": "Date",
|
2037
|
+
"1": "1-Pd",
|
2038
|
+
"2": "2-Pd",
|
2039
|
+
"3": "3-Pd",
|
2040
|
+
"4": "4-Pd",
|
2041
|
+
"5": "5-Pd",
|
2042
|
+
"10": "10-Pd",
|
2043
|
+
"15": "15-Pd",
|
2044
|
+
"22": "22-Pd",
|
2045
|
+
"30": "30-Pd",
|
2046
|
+
"T": "Trend",
|
2047
|
+
"V": "volume",
|
2048
|
+
"M": "MA-Signal",
|
2049
|
+
}
|
2050
|
+
|
2051
|
+
if self.config_manager.enablePortfolioCalculations:
|
2052
|
+
if 'RUNNER' not in os.environ.keys():
|
2053
|
+
task1 = PKTask("PortfolioLedger", long_running_fn=PortfolioCollection().getPortfoliosAsDataframe)
|
2054
|
+
task2 = PKTask("PortfolioLedgerSnapshots", long_running_fn=PortfolioCollection().getLedgerSummaryAsDataframe)
|
2055
|
+
tasks_list = [task1, task2]
|
2056
|
+
PKScheduler.scheduleTasks(tasks_list=tasks_list, label=f"Portfolio Calculations Report Export(Total={len(tasks_list)})", timeout=600)
|
2057
|
+
else:
|
2058
|
+
for task in tasks_list:
|
2059
|
+
task.long_running_fn(*(task,))
|
2060
|
+
|
2061
|
+
for task in tasks_list:
|
2062
|
+
if task.result is not None:
|
2063
|
+
self.show_backtest_results(task.result, sortKey=None, optionalName=task.taskName)
|
2064
|
+
|
2065
|
+
return summary_df, sorting, sort_keys
|
2066
|
+
|
2067
|
+
def show_sorted_backtest_data(self, backtest_df, summary_df, sort_keys):
|
2068
|
+
"""
|
2069
|
+
Show sorted backtest data with user interaction.
|
2070
|
+
|
2071
|
+
Args:
|
2072
|
+
backtest_df: Backtest dataframe
|
2073
|
+
summary_df: Summary dataframe
|
2074
|
+
sort_keys: Sort keys dictionary
|
2075
|
+
|
2076
|
+
Returns:
|
2077
|
+
bool: Whether sorting should continue
|
2078
|
+
"""
|
2079
|
+
OutputControls().printOutput(
|
2080
|
+
colorText.FAIL
|
2081
|
+
+ " [+] Would you like to sort the results?"
|
2082
|
+
+ colorText.END
|
2083
|
+
)
|
2084
|
+
|
2085
|
+
OutputControls().printOutput(
|
2086
|
+
colorText.GREEN
|
2087
|
+
+ " [+] Press :\n [+] s, v, t, m : sort by Stocks, Volume, Trend, MA-Signal\n [+] d : sort by date\n [+] 1,2,3...30 : sort by period\n [+] n : Exit sorting\n"
|
2088
|
+
+ colorText.END
|
2089
|
+
)
|
2090
|
+
|
2091
|
+
if self.default_answer is None:
|
2092
|
+
choice = input(
|
2093
|
+
colorText.FAIL + " [+] Select option:"
|
2094
|
+
)
|
2095
|
+
OutputControls().printOutput(colorText.END, end="")
|
2096
|
+
|
2097
|
+
if choice.upper() in sort_keys.keys():
|
2098
|
+
ConsoleUtility.PKConsoleTools.clearScreen(forceTop=True)
|
2099
|
+
self.show_backtest_results(backtest_df, sort_keys[choice.upper()])
|
2100
|
+
self.show_backtest_results(summary_df, optionalName="Summary")
|
2101
|
+
else:
|
2102
|
+
sorting = False
|
2103
|
+
else:
|
2104
|
+
OutputControls().printOutput("Finished backtesting!")
|
2105
|
+
sorting = False
|
2106
|
+
|
2107
|
+
return sorting
|
2108
|
+
|
2109
|
+
def show_backtest_results(self, backtest_df, sort_key="Stock", optional_name="backtest_result", choices=None):
|
2110
|
+
"""
|
2111
|
+
Show backtest results in formatted output.
|
2112
|
+
|
2113
|
+
Args:
|
2114
|
+
backtest_df: Backtest dataframe
|
2115
|
+
sort_key: Sort key for results
|
2116
|
+
optional_name: Optional name for the report
|
2117
|
+
choices: Choices string for filename
|
2118
|
+
"""
|
2119
|
+
pd.set_option("display.max_rows", 800)
|
2120
|
+
|
2121
|
+
if backtest_df is None or backtest_df.empty or len(backtest_df) < 1:
|
2122
|
+
OutputControls().printOutput("Empty backtest dataframe encountered! Cannot generate the backtest report")
|
2123
|
+
return
|
2124
|
+
|
2125
|
+
backtest_df.drop_duplicates(inplace=True)
|
2126
|
+
summary_text = f"Auto-generated in {round(self.elapsed_time,2)} sec. as of {PKDateUtilities.currentDateTime().strftime('%d-%m-%y %H:%M:%S IST')}\n{self.menu_choice_hierarchy.replace('Backtests','Growth of 10K' if optional_name=='Insights' else 'Backtests')}"
|
2127
|
+
last_summary_row = None
|
2128
|
+
|
2129
|
+
if "Summary" not in optional_name:
|
2130
|
+
if sort_key is not None and len(sort_key) > 0:
|
2131
|
+
backtest_df.sort_values(by=[sort_key], ascending=False, inplace=True)
|
2132
|
+
else:
|
2133
|
+
last_row = backtest_df.iloc[-1, :]
|
2134
|
+
|
2135
|
+
if last_row.iloc[0] == "SUMMARY":
|
2136
|
+
last_summary_row = pd.DataFrame(last_row).transpose()
|
2137
|
+
last_summary_row.set_index("Stock", inplace=True)
|
2138
|
+
last_summary_row = last_summary_row.iloc[:, last_summary_row.columns != "Stock"]
|
2139
|
+
|
2140
|
+
if "Insights" in optional_name:
|
2141
|
+
summary_text = f"{summary_text}\nActual returns at a portfolio level with 1-stock each based on selected scan-parameters:"
|
2142
|
+
else:
|
2143
|
+
summary_text = f"{summary_text}\nOverall Summary of (correctness of) Strategy Prediction Positive outcomes:"
|
2144
|
+
|
2145
|
+
tabulated_text = ""
|
2146
|
+
|
2147
|
+
if backtest_df is not None and len(backtest_df) > 0:
|
2148
|
+
try:
|
2149
|
+
tabulated_text = colorText.miniTabulator().tabulate(
|
2150
|
+
backtest_df,
|
2151
|
+
headers="keys",
|
2152
|
+
tablefmt=colorText.No_Pad_GridFormat,
|
2153
|
+
showindex=False,
|
2154
|
+
maxcolwidths=Utility.tools.getMaxColumnWidths(backtest_df)
|
2155
|
+
).encode("utf-8").decode(STD_ENCODING)
|
2156
|
+
except ValueError:
|
2157
|
+
OutputControls().printOutput("ValueError! Going ahead without any column width restrictions!")
|
2158
|
+
tabulated_text = colorText.miniTabulator().tabulate(
|
2159
|
+
backtest_df,
|
2160
|
+
headers="keys",
|
2161
|
+
tablefmt=colorText.No_Pad_GridFormat,
|
2162
|
+
showindex=False,
|
2163
|
+
).encode("utf-8").decode(STD_ENCODING)
|
2164
|
+
pass
|
2165
|
+
|
2166
|
+
OutputControls().printOutput(colorText.FAIL + summary_text + colorText.END + "\n")
|
2167
|
+
OutputControls().printOutput(tabulated_text + "\n")
|
2168
|
+
|
2169
|
+
choices, filename = self.get_backtest_report_filename(sort_key, optional_name, choices=choices)
|
2170
|
+
header_dict = {0: "<th></th>"}
|
2171
|
+
index = 1
|
2172
|
+
|
2173
|
+
for col in backtest_df.columns:
|
2174
|
+
if col != "Stock":
|
2175
|
+
header_dict[index] = f"<th>{col}</th>"
|
2176
|
+
index += 1
|
2177
|
+
|
2178
|
+
colored_text = backtest_df.to_html(index=False)
|
2179
|
+
summary_text = summary_text.replace("\n", "<br />")
|
2180
|
+
|
2181
|
+
if "Summary" in optional_name:
|
2182
|
+
summary_text = f"{summary_text}<br /><input type='checkbox' id='chkActualNumbers' name='chkActualNumbers' value='0'><label for='chkActualNumbers'>Sort by actual numbers (Stocks + Date combinations of results. Higher the count, better the prediction reliability)</label><br>"
|
2183
|
+
|
2184
|
+
colored_text = self.reformat_table(summary_text, header_dict, colored_text, sorting=True)
|
2185
|
+
filename = os.path.join(self.scan_output_directory(True), filename)
|
2186
|
+
|
2187
|
+
try:
|
2188
|
+
os.remove(filename)
|
2189
|
+
except Exception:
|
2190
|
+
pass
|
2191
|
+
finally:
|
2192
|
+
colored_text = colored_text.encode('utf-8').decode(STD_ENCODING)
|
2193
|
+
|
2194
|
+
with open(filename, "w") as f:
|
2195
|
+
f.write(colored_text)
|
2196
|
+
|
2197
|
+
if "RUNNER" in os.environ.keys():
|
2198
|
+
Committer.execOSCommand(f"git add {filename} -f >/dev/null 2>&1")
|
2199
|
+
|
2200
|
+
try:
|
2201
|
+
if self.config_manager.alwaysExportToExcel:
|
2202
|
+
excel_sheetname = filename.split(os.sep)[-1].replace("PKScreener_", "").replace(".html", "")
|
2203
|
+
PKAssetsManager.promptSaveResults(sheetName=excel_sheetname, df_save=backtest_df, defaultAnswer=self.user_passed_args.answerdefault, pastDate=None)
|
2204
|
+
except:
|
2205
|
+
pass
|
2206
|
+
|
2207
|
+
if last_summary_row is not None:
|
2208
|
+
oneline_text = last_summary_row.to_html(header=False, index=False)
|
2209
|
+
oneline_text = self.reformat_table(
|
2210
|
+
summary_text, header_dict, oneline_text, sorting=False
|
2211
|
+
)
|
2212
|
+
oneline_summary_file = f"PKScreener_{choices}_OneLine_{optional_name}.html"
|
2213
|
+
oneline_summary_file = os.path.join(
|
2214
|
+
self.scan_output_directory(True), oneline_summary_file
|
2215
|
+
)
|
2216
|
+
|
2217
|
+
try:
|
2218
|
+
os.remove(oneline_summary_file)
|
2219
|
+
except Exception:
|
2220
|
+
pass
|
2221
|
+
finally:
|
2222
|
+
oneline_text = f"{oneline_text}<td class='w'>{PKDateUtilities.currentDateTime().strftime('%Y/%m/%d')}</td><td class='w'>{round(self.elapsed_time,2)}</td>"
|
2223
|
+
|
2224
|
+
with open(oneline_summary_file, "w") as f:
|
2225
|
+
f.write(oneline_text)
|
2226
|
+
|
2227
|
+
if "RUNNER" in os.environ.keys():
|
2228
|
+
Committer.execOSCommand(f"git add {oneline_summary_file} -f >/dev/null 2>&1")
|
2229
|
+
|
2230
|
+
def scan_output_directory(self, backtest=False):
|
2231
|
+
"""
|
2232
|
+
Get the scan output directory path.
|
2233
|
+
|
2234
|
+
Args:
|
2235
|
+
backtest (bool): Whether for backtest results
|
2236
|
+
|
2237
|
+
Returns:
|
2238
|
+
str: Output directory path
|
2239
|
+
"""
|
2240
|
+
dir_name = 'actions-data-scan' if not backtest else "Backtest-Reports"
|
2241
|
+
output_folder = os.path.join(os.getcwd(), dir_name)
|
2242
|
+
|
2243
|
+
if not os.path.isdir(output_folder):
|
2244
|
+
OutputControls().printOutput("Creating actions-data-scan directory now...")
|
2245
|
+
os.makedirs(os.path.dirname(os.path.join(os.getcwd(), f"{dir_name}{os.sep}")), exist_ok=True)
|
2246
|
+
|
2247
|
+
return output_folder
|
2248
|
+
|
2249
|
+
def get_backtest_report_filename(self, sort_key="Stock", optional_name="backtest_result", choices=None):
|
2250
|
+
"""
|
2251
|
+
Get backtest report filename.
|
2252
|
+
|
2253
|
+
Args:
|
2254
|
+
sort_key: Sort key for filename
|
2255
|
+
optional_name: Optional name for filename
|
2256
|
+
choices: Choices string for filename
|
2257
|
+
|
2258
|
+
Returns:
|
2259
|
+
tuple: Choices string and filename
|
2260
|
+
"""
|
2261
|
+
if choices is None:
|
2262
|
+
choices = PKScanRunner.getFormattedChoices(self.user_passed_args, self.selected_choice).strip()
|
2263
|
+
|
2264
|
+
filename = f"PKScreener_{choices.strip()}_{optional_name.strip()}_{sort_key.strip() if sort_key is not None else 'Default'}Sorted.html"
|
2265
|
+
return choices.strip(), filename.strip()
|
2266
|
+
|
2267
|
+
def reformat_table(self, summary_text, header_dict, colored_text, sorting=True):
|
2268
|
+
"""
|
2269
|
+
Reformat table for HTML output.
|
2270
|
+
|
2271
|
+
Args:
|
2272
|
+
summary_text: Summary text
|
2273
|
+
header_dict: Header dictionary
|
2274
|
+
colored_text: Colored text content
|
2275
|
+
sorting (bool): Whether to enable sorting
|
2276
|
+
|
2277
|
+
Returns:
|
2278
|
+
str: Reformatted table HTML
|
2279
|
+
"""
|
2280
|
+
if sorting:
|
2281
|
+
table_text = "<!DOCTYPE html><html><head><script type='application/javascript' src='https://pkjmesra.github.io/pkjmesra/pkscreener/classes/tableSorting.js' ></script><style type='text/css'>body, table {background-color: black; color: white;} table, th, td {border: 1px solid white;} th {cursor: pointer; color:white; text-decoration:underline;} .r {color:red;font-weight:bold;} .br {border-color:green;border-width:medium;} .w {color:white;font-weight:bold;} .g {color:lightgreen;font-weight:bold;} .y {color:yellow;} .bg {background-color:darkslategrey;} .bb {background-color:black;} input#searchReports { width: 220px; } table thead tr th { background-color: black; position: sticky; z-index: 100; top: 0; } </style></head><body><span style='color:white;' >"
|
2282
|
+
colored_text = colored_text.replace(
|
2283
|
+
"<table", f"{table_text}{summary_text}<br /><input type='text' id='searchReports' onkeyup='searchReportsByAny()' placeholder='Search for stock/scan reports..' title='Type in a name/ID'><table")
|
2284
|
+
colored_text = colored_text.replace("<table ", "<table id='resultsTable' ")
|
2285
|
+
colored_text = colored_text.replace('<tr style="text-align: right;">', '<tr style="text-align: right;" class="header">')
|
2286
|
+
|
2287
|
+
for key in header_dict.keys():
|
2288
|
+
if key > 0:
|
2289
|
+
colored_text = colored_text.replace(
|
2290
|
+
header_dict[key], f"<th>{header_dict[key][4:]}"
|
2291
|
+
)
|
2292
|
+
else:
|
2293
|
+
colored_text = colored_text.replace(
|
2294
|
+
header_dict[key], f"<th>Stock{header_dict[key][4:]}"
|
2295
|
+
)
|
2296
|
+
else:
|
2297
|
+
colored_text = colored_text.replace('<table border="1" class="dataframe">', "")
|
2298
|
+
colored_text = colored_text.replace("<tbody>", "")
|
2299
|
+
colored_text = colored_text.replace("<tr>", "")
|
2300
|
+
colored_text = colored_text.replace("</tr>", "")
|
2301
|
+
colored_text = colored_text.replace("</tbody>", "")
|
2302
|
+
colored_text = colored_text.replace("</table>", "")
|
2303
|
+
|
2304
|
+
colored_text = colored_text.replace(colorText.BOLD, "")
|
2305
|
+
colored_text = colored_text.replace(f"{colorText.GREEN}", "<span class='g'>")
|
2306
|
+
colored_text = colored_text.replace(f"{colorText.FAIL}", "<span class='r'>")
|
2307
|
+
colored_text = colored_text.replace(f"{colorText.WARN}", "<span class='y'>")
|
2308
|
+
colored_text = colored_text.replace(f"{colorText.WHITE}", "<span class='w'>")
|
2309
|
+
colored_text = colored_text.replace("<td><span class='w'>", "<td class='br'><span class='w'>")
|
2310
|
+
colored_text = colored_text.replace(colorText.END, "</span>")
|
2311
|
+
colored_text = colored_text.replace("\n", "")
|
2312
|
+
|
2313
|
+
if sorting:
|
2314
|
+
colored_text = colored_text.replace("</table>", "</table></span></body></html>")
|
2315
|
+
|
2316
|
+
return colored_text
|
2317
|
+
|
2318
|
+
def tabulate_backtest_results(self, save_results, max_allowed=0, force=False):
|
2319
|
+
"""
|
2320
|
+
Tabulate backtest results for display.
|
2321
|
+
|
2322
|
+
Args:
|
2323
|
+
save_results: Results to save
|
2324
|
+
max_allowed: Maximum allowed results
|
2325
|
+
force (bool): Whether to force display
|
2326
|
+
|
2327
|
+
Returns:
|
2328
|
+
tuple: Tabulated summary and detail text
|
2329
|
+
"""
|
2330
|
+
if "PKDevTools_Default_Log_Level" not in os.environ.keys():
|
2331
|
+
if ("RUNNER" not in os.environ.keys()) or ("RUNNER" in os.environ.keys() and not force):
|
2332
|
+
return None, None
|
2333
|
+
|
2334
|
+
if not self.config_manager.showPastStrategyData:
|
2335
|
+
return None, None
|
2336
|
+
|
2337
|
+
tabulated_backtest_summary = ""
|
2338
|
+
tabulated_backtest_detail = ""
|
2339
|
+
summary_df, detail_df = self.get_summary_correctness_of_strategy(save_results)
|
2340
|
+
|
2341
|
+
if summary_df is not None and len(summary_df) > 0:
|
2342
|
+
tabulated_backtest_summary = colorText.miniTabulator().tabulate(
|
2343
|
+
summary_df,
|
2344
|
+
headers="keys",
|
2345
|
+
tablefmt=colorText.No_Pad_GridFormat,
|
2346
|
+
showindex=False,
|
2347
|
+
maxcolwidths=Utility.tools.getMaxColumnWidths(summary_df)
|
2348
|
+
).encode("utf-8").decode(STD_ENCODING)
|
2349
|
+
|
2350
|
+
if detail_df is not None and len(detail_df) > 0:
|
2351
|
+
if max_allowed != 0 and len(detail_df) > 2 * max_allowed:
|
2352
|
+
detail_df = detail_df.head(2 * max_allowed)
|
2353
|
+
|
2354
|
+
tabulated_backtest_detail = colorText.miniTabulator().tabulate(
|
2355
|
+
detail_df,
|
2356
|
+
headers="keys",
|
2357
|
+
tablefmt=colorText.No_Pad_GridFormat,
|
2358
|
+
showindex=False,
|
2359
|
+
maxcolwidths=Utility.tools.getMaxColumnWidths(detail_df)
|
2360
|
+
).encode("utf-8").decode(STD_ENCODING)
|
2361
|
+
|
2362
|
+
if tabulated_backtest_summary != "":
|
2363
|
+
OutputControls().printOutput(
|
2364
|
+
colorText.FAIL
|
2365
|
+
+ "\n [+] For chosen scan, summary of correctness from past: [Example, 70% of (100) under 1-Pd, means out of 100 stocks that were in the scan result in the past, 70% of them gained next day.)"
|
2366
|
+
+ colorText.END
|
2367
|
+
)
|
2368
|
+
OutputControls().printOutput(tabulated_backtest_summary)
|
2369
|
+
|
2370
|
+
if tabulated_backtest_detail != "":
|
2371
|
+
OutputControls().printOutput(
|
2372
|
+
colorText.FAIL
|
2373
|
+
+ "\n [+] 1 to 30 period gain/loss % on respective date for matching stocks from earlier predictions:[Example, 5% under 1-P")
|