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.
Files changed (63) hide show
  1. pkscreener-0.46.20250911.771.data/purelib/pkscreener/MainApplication.py +721 -0
  2. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/README.txt +5 -5
  3. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/AssetsManager.py +12 -12
  4. pkscreener-0.46.20250911.771.data/purelib/pkscreener/classes/MenuManager.py +2373 -0
  5. pkscreener-0.46.20250911.771.data/purelib/pkscreener/classes/PKScreenerMain.py +916 -0
  6. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/ScreeningStatistics.py +2 -0
  7. pkscreener-0.46.20250911.771.data/purelib/pkscreener/classes/__init__.py +1 -0
  8. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/pkscreenercli.py +1 -0
  9. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/requirements.txt +1 -0
  10. {pkscreener-0.46.20250909.768.dist-info → pkscreener-0.46.20250911.771.dist-info}/METADATA +7 -7
  11. pkscreener-0.46.20250911.771.dist-info/RECORD +61 -0
  12. pkscreener-0.46.20250909.768.data/purelib/pkscreener/classes/__init__.py +0 -1
  13. pkscreener-0.46.20250909.768.dist-info/RECORD +0 -58
  14. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/Disclaimer.txt +0 -0
  15. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/LICENSE-Others.txt +0 -0
  16. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/LICENSE.txt +0 -0
  17. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/LogoWM.txt +0 -0
  18. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/__init__.py +0 -0
  19. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/ArtTexts.py +0 -0
  20. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/Backtest.py +0 -0
  21. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/Barometer.py +0 -0
  22. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/BaseScreeningStatistics.py +0 -0
  23. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/CandlePatterns.py +0 -0
  24. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/Changelog.py +0 -0
  25. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/ConfigManager.py +0 -0
  26. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/ConsoleMenuUtility.py +0 -0
  27. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/ConsoleUtility.py +0 -0
  28. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/Fetcher.py +0 -0
  29. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/GlobalStore.py +0 -0
  30. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/ImageUtility.py +0 -0
  31. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/MarketMonitor.py +0 -0
  32. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/MarketStatus.py +0 -0
  33. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/MenuOptions.py +0 -0
  34. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/Messenger.py +0 -0
  35. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/OtaUpdater.py +0 -0
  36. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKAnalytics.py +0 -0
  37. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKDataService.py +0 -0
  38. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKDemoHandler.py +0 -0
  39. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKMarketOpenCloseAnalyser.py +0 -0
  40. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKPremiumHandler.py +0 -0
  41. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKScanRunner.py +0 -0
  42. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKScheduledTaskProgress.py +0 -0
  43. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKScheduler.py +0 -0
  44. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKSpreadsheets.py +0 -0
  45. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKTask.py +0 -0
  46. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PKUserRegistration.py +0 -0
  47. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/Pktalib.py +0 -0
  48. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/Portfolio.py +0 -0
  49. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/PortfolioXRay.py +0 -0
  50. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/StockScreener.py +0 -0
  51. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/StockSentiment.py +0 -0
  52. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/UserMenuChoicesHandler.py +0 -0
  53. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/Utility.py +0 -0
  54. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/WorkflowManager.py +0 -0
  55. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/classes/keys.py +0 -0
  56. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/courbd.ttf +0 -0
  57. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/globals.py +0 -0
  58. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/pkscreener.ini +0 -0
  59. {pkscreener-0.46.20250909.768.data → pkscreener-0.46.20250911.771.data}/purelib/pkscreener/pkscreenerbot.py +0 -0
  60. {pkscreener-0.46.20250909.768.dist-info → pkscreener-0.46.20250911.771.dist-info}/WHEEL +0 -0
  61. {pkscreener-0.46.20250909.768.dist-info → pkscreener-0.46.20250911.771.dist-info}/entry_points.txt +0 -0
  62. {pkscreener-0.46.20250909.768.dist-info → pkscreener-0.46.20250911.771.dist-info}/licenses/LICENSE +0 -0
  63. {pkscreener-0.46.20250909.768.dist-info → pkscreener-0.46.20250911.771.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,721 @@
1
+ """
2
+ Main application refactoring for PKScreener
3
+ This module breaks down the massive main function into manageable classes
4
+ while preserving all existing functionality.
5
+ """
6
+
7
+ import multiprocessing
8
+ import os
9
+ import sys
10
+ import time
11
+ import pandas as pd
12
+ from datetime import timedelta
13
+ from typing import Dict, List, Tuple, Any, Optional
14
+
15
+ # Import all necessary modules (preserving original imports)
16
+ import urllib.error
17
+ from alive_progress import alive_bar
18
+
19
+ # Import project-specific modules
20
+ from pkscreener.classes import colorText, Utility, PKDateUtilities, ConsoleUtility, PKScanRunner
21
+ from pkscreener.classes.ConfigManager import ConfigManager
22
+ from pkscreener.classes.Fetcher import fetcher
23
+ from pkscreener.classes.PKMenu import m0, m1, m2, m3, m4
24
+ from pkscreener.classes.PortfolioXRay import PortfolioXRay
25
+ from pkscreener.classes.log import default_logger
26
+ from pkscreener.classes.PKAnalytics import PKAnalyticsService
27
+ from pkscreener.classes.PKPremiumHandler import PKPremiumHandler
28
+ from pkscreener.classes.PKSpreadsheets import PKSpreadsheets
29
+ from pkscreener.globals import *
30
+
31
+ class ApplicationState:
32
+ """Manages the global application state and configuration"""
33
+
34
+ def __init__(self, userArgs=None):
35
+ self.userArgs = userArgs
36
+ self.initialize_globals()
37
+ self.setup_initial_state()
38
+
39
+ def initialize_globals(self):
40
+ """Initialize all global variables with their default values"""
41
+ global lastScanOutputStockCodes, scanCycleRunning, runCleanUp, test_messages_queue
42
+ global show_saved_diff_results, criteria_dateTime, analysis_dict, mp_manager
43
+ global listStockCodes, screenResults, selectedChoice, defaultAnswer, menuChoiceHierarchy
44
+ global screenCounter, screenResultsCounter, stockDictPrimary, stockDictSecondary
45
+ global userPassedArgs, loadedStockData, keyboardInterruptEvent, loadCount, maLength
46
+ global newlyListedOnly, keyboardInterruptEventFired, strategyFilter, elapsed_time, start_time
47
+
48
+ selectedChoice = {"0": "", "1": "", "2": "", "3": "", "4": ""}
49
+ elapsed_time = 0 if not scanCycleRunning else elapsed_time
50
+ start_time = 0 if not scanCycleRunning else start_time
51
+
52
+ def setup_initial_state(self):
53
+ """Set up the initial application state based on user arguments"""
54
+ global testing, testBuild, downloadOnly, startupoptions, user, defaultAnswer, userPassedArgs
55
+ global runOptionName, options, strategyFilter, test_messages_queue
56
+
57
+ testing = False if self.userArgs is None else (self.userArgs.testbuild and self.userArgs.prodbuild)
58
+ testBuild = False if self.userArgs is None else (self.userArgs.testbuild and not testing)
59
+ downloadOnly = False if self.userArgs is None else self.userArgs.download
60
+ startupoptions = None if self.userArgs is None else self.userArgs.options
61
+ user = None if self.userArgs is None else self.userArgs.user
62
+ defaultAnswer = None if self.userArgs is None else self.userArgs.answerdefault
63
+ userPassedArgs = self.userArgs
64
+ runOptionName = ""
65
+ options = []
66
+ strategyFilter = []
67
+ test_messages_queue = []
68
+
69
+ describeUser()
70
+
71
+
72
+ class MenuHandler:
73
+ """Handles all menu-related operations and user interactions"""
74
+
75
+ def __init__(self, app_state):
76
+ self.app_state = app_state
77
+ self.configManager = ConfigManager()
78
+
79
+ def process_top_level_menu(self):
80
+ """Process the top-level menu options"""
81
+ global screenResults, saveResults, options, menuOption, indexOption, executeOption
82
+
83
+ screenResults, saveResults = PKScanRunner.initDataframes()
84
+ options, menuOption, indexOption, executeOption = getTopLevelMenuChoices(
85
+ self.app_state.startupoptions, self.app_state.testBuild,
86
+ self.app_state.downloadOnly, defaultAnswer=self.app_state.defaultAnswer
87
+ )
88
+
89
+ selectedMenu = initExecution(menuOption=menuOption)
90
+ menuOption = selectedMenu.menuKey
91
+
92
+ return selectedMenu, menuOption, indexOption, executeOption
93
+
94
+ def handle_premium_checks(self, selectedMenu):
95
+ """Check if user has premium access for premium features"""
96
+ global menuOption, indexOption, executeOption
97
+
98
+ if menuOption in ["F", "M", "S", "B", "G", "C", "P", "D"] or selectedMenu.isPremium:
99
+ ensureMenusLoaded(menuOption, indexOption, executeOption)
100
+ if not PKPremiumHandler.hasPremium(selectedMenu):
101
+ PKAnalyticsService().send_event(f"non_premium_user_{menuOption}_{indexOption}_{executeOption}")
102
+ PKAnalyticsService().send_event("app_exit")
103
+ sys.exit(0)
104
+
105
+ def handle_special_menu_options(self, menuOption, selectedMenu):
106
+ """Handle special menu options like monitor, download, etc."""
107
+ if menuOption in ["M", "D", "I", "L", "F"]:
108
+ self.handle_monitor_download_options(menuOption, selectedMenu)
109
+ elif menuOption in ["P"]:
110
+ self.handle_predefined_scans()
111
+ elif menuOption in ["X", "T", "E", "Y", "U", "H", "C"]:
112
+ self.handle_scanner_menu_options()
113
+ elif menuOption in ["B", "G"]:
114
+ self.handle_backtest_options()
115
+ elif menuOption in ["S"]:
116
+ self.handle_strategy_options()
117
+
118
+ def handle_monitor_download_options(self, menuOption, selectedMenu):
119
+ """Handle monitor and download menu options"""
120
+ launcher = f'"{sys.argv[0]}"' if " " in sys.argv[0] else sys.argv[0]
121
+ launcher = f"python3.12 {launcher}" if (launcher.endswith(".py\"") or launcher.endswith(".py")) else launcher
122
+
123
+ if menuOption in ["M"]:
124
+ OutputControls().printOutput(f"{colorText.GREEN}Launching PKScreener in monitoring mode. If it does not launch, please try with the following:{colorText.END}\n{colorText.FAIL}{launcher} --systemlaunched -a Y -m 'X'{colorText.END}\n{colorText.WARN}Press Ctrl + C to exit monitoring mode.{colorText.END}")
125
+ PKAnalyticsService().send_event(f"monitor_{menuOption}")
126
+ time.sleep(2)
127
+ os.system(f"{launcher} --systemlaunched -a Y -m 'X'")
128
+ elif menuOption in ["D"]:
129
+ self.handle_download_options(selectedMenu, launcher)
130
+ elif menuOption in ["L"]:
131
+ PKAnalyticsService().send_event(f"{menuOption}")
132
+ OutputControls().printOutput(f"{colorText.GREEN}Launching PKScreener to collect logs. If it does not launch, please try with the following:{colorText.END}\n{colorText.FAIL}{launcher} -a Y -l{colorText.END}\n{colorText.WARN}Press Ctrl + C to exit at any time.{colorText.END}")
133
+ time.sleep(2)
134
+ os.system(f"{launcher} -a Y -l")
135
+ elif menuOption in ["F"]:
136
+ self.handle_favorites_option()
137
+
138
+ def handle_download_options(self, selectedMenu, launcher):
139
+ """Handle download-specific menu options"""
140
+ ConsoleUtility.PKConsoleTools.clearScreen(forceTop=True)
141
+ m1.renderForMenu(selectedMenu)
142
+ selDownloadOption = input(colorText.FAIL + " [+] Select option: ") or "D"
143
+ OutputControls().printOutput(colorText.END, end="")
144
+
145
+ if selDownloadOption.upper() == "D":
146
+ OutputControls().printOutput(f"{colorText.GREEN}Launching PKScreener to Download daily OHLC data. If it does not launch, please try with the following:{colorText.END}\n{colorText.FAIL}{launcher} -a Y -e -d{colorText.END}\n{colorText.WARN}Press Ctrl + C to exit at any time.{colorText.END}")
147
+ PKAnalyticsService().send_event(f"{menuOption}_{selDownloadOption.upper()}")
148
+ time.sleep(2)
149
+ os.system(f"{launcher} -a Y -e -d")
150
+ return None, None
151
+ # ... handle other download options
152
+
153
+ def handle_favorites_option(self):
154
+ """Handle favorites menu option"""
155
+ global indexOption, selectedChoice, listStockCodes
156
+
157
+ PKAnalyticsService().send_event(f"{menuOption}")
158
+ indexOption = 0
159
+ selectedChoice["0"] = "F"
160
+ selectedChoice["1"] = "0"
161
+ executeOption = None
162
+ shouldSuppress = not OutputControls().enableMultipleLineOutput
163
+
164
+ if userPassedArgs is not None and userPassedArgs.options is not None and len(userPassedArgs.options.split(":")) >= 3:
165
+ stockOptions = userPassedArgs.options.split(":")
166
+ stockOptions = userPassedArgs.options.split(":")[2 if len(stockOptions)<=3 else 3]
167
+ listStockCodes = stockOptions.replace(".",",").split(",")
168
+
169
+ if listStockCodes is None or len(listStockCodes) == 0:
170
+ with SuppressOutput(suppress_stderr=shouldSuppress, suppress_stdout=shouldSuppress):
171
+ listStockCodes = fetcher.fetchStockCodes(tickerOption=0, stockCode=None)
172
+
173
+ ConsoleUtility.PKConsoleTools.clearScreen(clearAlways=True, forceTop=True)
174
+
175
+ def handle_predefined_scans(self):
176
+ """Handle predefined scan options"""
177
+ global predefinedOption, selPredefinedOption, selIndexOption, selectedChoice
178
+
179
+ predefinedOption = None
180
+ selPredefinedOption = None
181
+ selIndexOption = None
182
+
183
+ if len(options) >= 3:
184
+ predefinedOption = str(options[1]) if str(options[1]).isnumeric() else '1'
185
+ selPredefinedOption = str(options[2]) if str(options[2]).isnumeric() else '1'
186
+ if len(options) >= 4:
187
+ selIndexOption = str(options[3]) if str(options[3]).isnumeric() else '12'
188
+
189
+ selectedChoice["0"] = "P"
190
+ updateMenuChoiceHierarchy()
191
+ selectedMenu = m0.find(menuOption)
192
+ m1.renderForMenu(selectedMenu, asList=(userPassedArgs is not None and userPassedArgs.options is not None))
193
+
194
+ # ... handle predefined scan logic
195
+
196
+
197
+ class ScannerHandler:
198
+ """Handles scanner operations and screening logic"""
199
+
200
+ def __init__(self, app_state):
201
+ self.app_state = app_state
202
+ self.configManager = ConfigManager()
203
+
204
+ def execute_scanner(self, menuOption, indexOption, executeOption):
205
+ """Execute the selected scanner with appropriate parameters"""
206
+ global volumeRatio, minRSI, maxRSI, insideBarToLookback, respChartPattern
207
+ global daysForLowestVolume, reversalOption, maLength
208
+
209
+ volumeRatio = self.configManager.volumeRatio
210
+
211
+ if executeOption == 3:
212
+ userPassedArgs.maxdisplayresults = max(self.configManager.maxdisplayresults, 2000)
213
+ elif executeOption == 4:
214
+ daysForLowestVolume = self.handle_scanner_execute_option_4(executeOption, options)
215
+ elif executeOption == 5:
216
+ self.handle_rsi_scanner(executeOption, options)
217
+ elif executeOption == 6:
218
+ self.handle_reversal_scanner(executeOption, options)
219
+ elif executeOption == 7:
220
+ self.handle_chart_pattern_scanner(executeOption, options)
221
+ # ... handle other execute options
222
+
223
+ def handle_rsi_scanner(self, executeOption, options):
224
+ """Handle RSI scanner configuration"""
225
+ global minRSI, maxRSI, selectedMenu
226
+
227
+ selectedMenu = m2.find(str(executeOption))
228
+ if len(options) >= 5:
229
+ if str(options[3]).isnumeric():
230
+ minRSI = int(options[3])
231
+ maxRSI = int(options[4])
232
+ elif str(options[3]).upper() == "D" or userPassedArgs.systemlaunched:
233
+ minRSI = 60
234
+ maxRSI = 75
235
+ else:
236
+ minRSI, maxRSI = ConsoleMenuUtility.PKConsoleMenuTools.promptRSIValues()
237
+
238
+ if not minRSI and not maxRSI:
239
+ OutputControls().printOutput(
240
+ colorText.FAIL
241
+ + "\n [+] Error: Invalid values for RSI! Values should be in range of 0 to 100. Please try again!"
242
+ + colorText.END
243
+ )
244
+ OutputControls().takeUserInput("Press <Enter> to continue...")
245
+ return None, None
246
+
247
+ def handle_reversal_scanner(self, executeOption, options):
248
+ """Handle reversal scanner configuration"""
249
+ global reversalOption, maLength, selectedMenu
250
+
251
+ selectedMenu = m2.find(str(executeOption))
252
+ if len(options) >= 4:
253
+ reversalOption = int(options[3])
254
+ if reversalOption in [4, 6, 7, 10]:
255
+ if len(options) >= 5:
256
+ if str(options[4]).isnumeric():
257
+ maLength = int(options[4])
258
+ elif str(options[4]).upper() == "D" or userPassedArgs.systemlaunched:
259
+ maLength = 50 if reversalOption == 4 else (3 if reversalOption in [7] else (2 if reversalOption in [10] else 7))
260
+ elif defaultAnswer == "Y" and user is not None:
261
+ maLength = 50 if reversalOption == 4 else (3 if reversalOption == 7 else 7)
262
+ else:
263
+ reversalOption, maLength = ConsoleMenuUtility.PKConsoleMenuTools.promptReversalScreening(selectedMenu)
264
+ else:
265
+ reversalOption, maLength = ConsoleMenuUtility.PKConsoleMenuTools.promptReversalScreening(selectedMenu)
266
+
267
+ if reversalOption is None or reversalOption == 0 or maLength == 0:
268
+ return None, None
269
+ else:
270
+ selectedChoice["3"] = str(reversalOption)
271
+ if str(reversalOption) in ["7", "10"]:
272
+ selectedChoice["4"] = str(maLength)
273
+
274
+
275
+ class BacktestHandler:
276
+ """Handles backtesting operations and results processing"""
277
+
278
+ def __init__(self, app_state):
279
+ self.app_state = app_state
280
+ self.configManager = ConfigManager()
281
+
282
+ def handle_backtest_options(self):
283
+ """Handle backtest menu options"""
284
+ global backtestPeriod
285
+
286
+ backtestPeriod = 0
287
+ if len(options) >= 2:
288
+ if str(indexOption).isnumeric():
289
+ backtestPeriod = int(indexOption)
290
+ if len(options) >= 4:
291
+ indexOption = executeOption
292
+ executeOption = options[3]
293
+ del options[1]
294
+
295
+ indexOption, executeOption, backtestPeriod = takeBacktestInputs(
296
+ str(menuOption).upper(), indexOption, executeOption, backtestPeriod
297
+ )
298
+ backtestPeriod = backtestPeriod * self.configManager.backtestPeriodFactor
299
+
300
+ def process_backtest_results(self, backtest_df):
301
+ """Process and display backtest results"""
302
+ if backtest_df is not None and len(backtest_df) > 0:
303
+ ConsoleUtility.PKConsoleTools.clearScreen(forceTop=True)
304
+ df_xray = prepareGroupedXRay(backtestPeriod, backtest_df)
305
+ summary_df, sorting, sortKeys = FinishBacktestDataCleanup(backtest_df, df_xray)
306
+
307
+ while sorting:
308
+ sorting = showSortedBacktestData(backtest_df, summary_df, sortKeys)
309
+
310
+ if defaultAnswer is None:
311
+ OutputControls().takeUserInput("Press <Enter> to continue...")
312
+ else:
313
+ OutputControls().printOutput("Finished backtesting with no results to show!")
314
+
315
+
316
+ class StrategyHandler:
317
+ """Handles strategy-related operations and filtering"""
318
+
319
+ def __init__(self, app_state):
320
+ self.app_state = app_state
321
+
322
+ def handle_strategy_options(self):
323
+ """Handle strategy menu options"""
324
+ global userOption, strategyFilter, menuOption, indexOption, executeOption, selectedChoice
325
+
326
+ if len(options) >= 2:
327
+ userOption = options[1]
328
+
329
+ if defaultAnswer is None:
330
+ selectedMenu = m0.find(menuOption)
331
+ m1.strategyNames = PortfolioXRay.strategyNames()
332
+ m1.renderForMenu(selectedMenu=selectedMenu)
333
+
334
+ try:
335
+ userOption = input(colorText.FAIL + " [+] Select option: ")
336
+ OutputControls().printOutput(colorText.END, end="")
337
+
338
+ if userOption == "":
339
+ userOption = "37" # NoFilter
340
+ elif userOption == "38":
341
+ userOption = input(colorText.FAIL + " [+] Enter Exact Pattern name:")
342
+ OutputControls().printOutput(colorText.END, end="")
343
+
344
+ if userOption == "":
345
+ userOption = "37" # NoFilter
346
+ else:
347
+ strategyFilter.append(f"[P]{userOption}")
348
+ userOption = "38"
349
+ except EOFError:
350
+ userOption = "37" # NoFilter
351
+ except Exception as e:
352
+ default_logger().debug(e, exc_info=True)
353
+
354
+ userOption = userOption.upper()
355
+
356
+ if userOption == "M":
357
+ ConsoleUtility.PKConsoleTools.clearScreen(forceTop=True)
358
+ return None, None
359
+ elif userOption == "Z":
360
+ handleExitRequest(userOption)
361
+ return None, None
362
+ elif userOption == "S":
363
+ self.handle_strategy_summary()
364
+ else:
365
+ self.apply_strategy_filters(userOption)
366
+
367
+ def handle_strategy_summary(self):
368
+ """Handle strategy summary display"""
369
+ OutputControls().printOutput(
370
+ colorText.GREEN
371
+ + " [+] Collecting all metrics for summarising..."
372
+ + colorText.END
373
+ )
374
+
375
+ savedValue = configManager.showPastStrategyData
376
+ configManager.showPastStrategyData = True
377
+ df_all = PortfolioXRay.summariseAllStrategies()
378
+
379
+ if df_all is not None and len(df_all) > 0:
380
+ OutputControls().printOutput(
381
+ colorText.miniTabulator().tabulate(
382
+ df_all,
383
+ headers="keys",
384
+ tablefmt=colorText.No_Pad_GridFormat,
385
+ showindex=False,
386
+ maxcolwidths=Utility.tools.getMaxColumnWidths(df_all)
387
+ ).encode("utf-8").decode(STD_ENCODING)
388
+ )
389
+ showBacktestResults(df_all, sortKey="Scanner", optionalName="InsightsSummary")
390
+ else:
391
+ OutputControls().printOutput("[!] Nothing to show here yet. Check back later.")
392
+
393
+ configManager.showPastStrategyData = savedValue
394
+
395
+ if defaultAnswer is None:
396
+ OutputControls().takeUserInput("Press <Enter> to continue...")
397
+
398
+ return None, None
399
+
400
+ def apply_strategy_filters(self, userOption):
401
+ """Apply selected strategy filters"""
402
+ global strategyFilter, menuOption, indexOption, executeOption, selectedChoice
403
+
404
+ userOptions = userOption.split(",")
405
+ for usrOption in userOptions:
406
+ strategyFilter.append(m1.find(usrOption).menuText.strip())
407
+
408
+ menuOption, indexOption, executeOption, selectedChoice = getScannerMenuChoices(
409
+ self.app_state.testBuild or self.app_state.testing,
410
+ self.app_state.downloadOnly,
411
+ self.app_state.startupoptions,
412
+ menuOption="X",
413
+ indexOption=indexOption,
414
+ executeOption=executeOption,
415
+ defaultAnswer=self.app_state.defaultAnswer,
416
+ user=self.app_state.user,
417
+ )
418
+
419
+
420
+ class DataLoader:
421
+ """Handles data loading and preparation for screening"""
422
+
423
+ def __init__(self, app_state):
424
+ self.app_state = app_state
425
+ self.configManager = ConfigManager()
426
+
427
+ def load_stock_data(self, menuOption, indexOption, downloadOnly, listStockCodes):
428
+ """Load stock data for screening"""
429
+ global stockDictPrimary, stockDictSecondary, loadedStockData
430
+
431
+ loadedStockData = loadedStockData and stockDictPrimary is not None and len(stockDictPrimary) > 0
432
+
433
+ if (menuOption in ["X", "B", "G", "S", "F"] and not loadedStockData) or (
434
+ self.configManager.cacheEnabled and not loadedStockData and not self.app_state.testing
435
+ ):
436
+ try:
437
+ import tensorflow as tf
438
+ with tf.device("/device:GPU:0"):
439
+ stockDictPrimary, stockDictSecondary = loadDatabaseOrFetch(
440
+ downloadOnly, listStockCodes, menuOption, indexOption
441
+ )
442
+ except:
443
+ stockDictPrimary, stockDictSecondary = loadDatabaseOrFetch(
444
+ downloadOnly, listStockCodes, menuOption, indexOption
445
+ )
446
+
447
+ return len(stockDictPrimary) if stockDictPrimary is not None else 0
448
+
449
+
450
+ class MainApplication:
451
+ """Main application class that orchestrates all components"""
452
+
453
+ def __init__(self, userArgs=None):
454
+ self.app_state = ApplicationState(userArgs)
455
+ self.menu_handler = MenuHandler(self.app_state)
456
+ self.scanner_handler = ScannerHandler(self.app_state)
457
+ self.backtest_handler = BacktestHandler(self.app_state)
458
+ self.strategy_handler = StrategyHandler(self.app_state)
459
+ self.data_loader = DataLoader(self.app_state)
460
+ self.configManager = ConfigManager()
461
+
462
+ def main(self, userArgs=None, optionalFinalOutcome_df=None):
463
+ """
464
+ Main entry point for the application
465
+ Refactored from the original massive main function
466
+
467
+ Args:
468
+ userArgs: Command line arguments passed by user
469
+ optionalFinalOutcome_df: Optional dataframe for final results
470
+
471
+ Returns:
472
+ Tuple of screen results and save results
473
+ """
474
+ global keyboardInterruptEventFired
475
+
476
+ # Check for keyboard interrupt
477
+ if keyboardInterruptEventFired:
478
+ return None, None
479
+
480
+ # Handle intraday analysis if requested
481
+ if self.handle_intraday_analysis(userArgs, optionalFinalOutcome_df):
482
+ savedAnalysisDict = analysis_dict.get(firstScanKey)
483
+ return analysisFinalResults(
484
+ savedAnalysisDict.get("S1"),
485
+ savedAnalysisDict.get("S2"),
486
+ optionalFinalOutcome_df,
487
+ None
488
+ )
489
+
490
+ # Initialize multiprocessing and market monitor
491
+ self.initialize_multiprocessing()
492
+ self.initialize_market_monitor()
493
+
494
+ # Handle cleanup if needed
495
+ if self.handle_cleanup(userArgs):
496
+ cleanupLocalResults()
497
+
498
+ # Process menu options
499
+ selectedMenu, menuOption, indexOption, executeOption = self.menu_handler.process_top_level_menu()
500
+
501
+ # Check premium access
502
+ self.menu_handler.handle_premium_checks(selectedMenu)
503
+
504
+ # Handle special menu options
505
+ self.menu_handler.handle_special_menu_options(menuOption, selectedMenu)
506
+
507
+ # Handle scanner menu options
508
+ if menuOption in ["X", "T", "E", "Y", "U", "H", "C"]:
509
+ menuOption, indexOption, executeOption, selectedChoice = getScannerMenuChoices(
510
+ self.app_state.testBuild or self.app_state.testing,
511
+ self.app_state.downloadOnly,
512
+ self.app_state.startupoptions,
513
+ menuOption=menuOption,
514
+ indexOption=indexOption,
515
+ executeOption=executeOption,
516
+ defaultAnswer=self.app_state.defaultAnswer,
517
+ user=self.app_state.user,
518
+ )
519
+
520
+ # Handle menu options X, B, G
521
+ handleMenu_XBG(menuOption, indexOption, executeOption)
522
+
523
+ # Check for exit request
524
+ if str(indexOption).upper() == "M" or str(executeOption).upper() == "M":
525
+ ConsoleUtility.PKConsoleTools.clearScreen(forceTop=True)
526
+ return None, None
527
+
528
+ # Prepare stocks for screening
529
+ listStockCodes = handleRequestForSpecificStocks(options, indexOption)
530
+ handleExitRequest(executeOption)
531
+
532
+ if executeOption is None:
533
+ executeOption = 0
534
+ executeOption = int(executeOption)
535
+
536
+ # Execute the selected scanner
537
+ self.scanner_handler.execute_scanner(menuOption, indexOption, executeOption)
538
+
539
+ # Load stock data
540
+ loadCount = self.data_loader.load_stock_data(
541
+ menuOption, indexOption, self.app_state.downloadOnly, listStockCodes
542
+ )
543
+
544
+ # Run the screening process
545
+ screenResults, saveResults = self.run_screening_process(
546
+ menuOption, indexOption, executeOption, listStockCodes, loadCount
547
+ )
548
+
549
+ # Process results
550
+ if menuOption in ["X", "C", "F"] and (userPassedArgs.monitor is None or self.configManager.alwaysExportToExcel) or ("|" not in userPassedArgs.options and menuOption not in ["B"]):
551
+ finishScreening(
552
+ self.app_state.downloadOnly,
553
+ self.app_state.testing,
554
+ stockDictPrimary,
555
+ self.configManager,
556
+ loadCount,
557
+ self.app_state.testBuild,
558
+ screenResults,
559
+ saveResults,
560
+ self.app_state.user,
561
+ )
562
+
563
+ # Handle backtest results
564
+ if menuOption == "B":
565
+ self.backtest_handler.process_backtest_results(backtest_df)
566
+
567
+ # Reset configuration
568
+ resetConfigToDefault()
569
+
570
+ # Save to Google Sheets if configured
571
+ self.save_to_google_sheets(saveResults)
572
+
573
+ # Handle pinned menu
574
+ self.handle_pinned_menu(saveResults, menuOption)
575
+
576
+ return screenResults, saveResults
577
+
578
+ def run_screening_process(self, menuOption, indexOption, executeOption, listStockCodes, loadCount):
579
+ """Run the screening process with appropriate parameters"""
580
+ global screenResults, saveResults, backtest_df
581
+
582
+ # Prepare screening parameters
583
+ samplingDuration, fillerPlaceHolder, actualHistoricalDuration = PKScanRunner.getScanDurationParameters(
584
+ self.app_state.testing, menuOption
585
+ )
586
+
587
+ totalStocksInReview = 0
588
+ savedStocksCount = 0
589
+ downloadedRecently = False
590
+ items = []
591
+ backtest_df = None
592
+
593
+ # Get progress bar style
594
+ bar, spinner = Utility.tools.getProgressbarStyle()
595
+
596
+ # Begin screening process
597
+ OutputControls().printOutput(f"{colorText.GREEN} [+] Adding stocks to the queue...{colorText.END}")
598
+
599
+ with alive_bar(actualHistoricalDuration, bar=bar, spinner=spinner) as progressbar:
600
+ while actualHistoricalDuration >= 0:
601
+ daysInPast = PKScanRunner.getBacktestDaysForScan(
602
+ userPassedArgs, backtestPeriod, menuOption, actualHistoricalDuration
603
+ )
604
+
605
+ try:
606
+ listStockCodes, savedStocksCount, pastDate = PKScanRunner.getStocksListForScan(
607
+ userPassedArgs, menuOption, totalStocksInReview, downloadedRecently, daysInPast
608
+ ) if menuOption not in ["C"] else (listStockCodes, 0, "")
609
+ except KeyboardInterrupt:
610
+ try:
611
+ keyboardInterruptEvent.set()
612
+ keyboardInterruptEventFired = True
613
+ actualHistoricalDuration = -1
614
+ break
615
+ except KeyboardInterrupt:
616
+ pass
617
+ OutputControls().printOutput(
618
+ colorText.FAIL
619
+ + "\n [+] Terminating Script, Please wait..."
620
+ + colorText.END
621
+ )
622
+
623
+ exchangeName = "NASDAQ" if (indexOption == 15 or (self.configManager.defaultIndex == 15 and indexOption == 0)) else "INDIA"
624
+ runOptionName = PKScanRunner.getFormattedChoices(userPassedArgs, selectedChoice)
625
+
626
+ if ((":0:" in runOptionName or "_0_" in runOptionName) and userPassedArgs.progressstatus is not None) or userPassedArgs.progressstatus is not None:
627
+ runOptionName = userPassedArgs.progressstatus.split("=>")[0].split(" [+] ")[1]
628
+
629
+ if menuOption in ["F"]:
630
+ if "^NSEI" in listStockCodes:
631
+ listStockCodes.remove("^NSEI")
632
+ items = PKScanRunner.addScansWithDefaultParams(
633
+ userPassedArgs, self.app_state.testing, self.app_state.testBuild,
634
+ newlyListedOnly, self.app_state.downloadOnly, backtestPeriod,
635
+ listStockCodes, menuOption, exchangeName, executeOption, volumeRatio,
636
+ items, daysInPast, runOption=f"{userPassedArgs.options} =>{runOptionName} => {menuChoiceHierarchy}"
637
+ )
638
+ else:
639
+ PKScanRunner.addStocksToItemList(
640
+ userPassedArgs, self.app_state.testing, self.app_state.testBuild,
641
+ newlyListedOnly, self.app_state.downloadOnly, minRSI, maxRSI,
642
+ insideBarToLookback, respChartPattern, daysForLowestVolume,
643
+ backtestPeriod, reversalOption, maLength, listStockCodes,
644
+ menuOption, exchangeName, executeOption, volumeRatio, items,
645
+ daysInPast, runOption=f"{userPassedArgs.options} =>{runOptionName} => {menuChoiceHierarchy}"
646
+ )
647
+
648
+ if savedStocksCount > 0:
649
+ progressbar.text(
650
+ colorText.GREEN
651
+ + f"Total Stocks: {len(items)}. Added {savedStocksCount} to Stocks from {pastDate} saved from earlier..."
652
+ + colorText.END
653
+ )
654
+
655
+ fillerPlaceHolder = fillerPlaceHolder + 1
656
+ actualHistoricalDuration = samplingDuration - fillerPlaceHolder
657
+
658
+ if actualHistoricalDuration >= 0:
659
+ progressbar()
660
+
661
+ # Run the scan with parameters
662
+ screenResults, saveResults, backtest_df, tasks_queue, results_queue, consumers, logging_queue = PKScanRunner.runScanWithParams(
663
+ userPassedArgs, keyboardInterruptEvent, screenCounter, screenResultsCounter,
664
+ stockDictPrimary, stockDictSecondary, self.app_state.testing, backtestPeriod,
665
+ menuOption, executeOption, samplingDuration, items, screenResults, saveResults,
666
+ backtest_df, scanningCb=runScanners, tasks_queue=tasks_queue,
667
+ results_queue=results_queue, consumers=consumers, logging_queue=logging_queue
668
+ )
669
+
670
+ return screenResults, saveResults
671
+
672
+ def save_to_google_sheets(self, saveResults):
673
+ """Save results to Google Sheets if configured"""
674
+ if 'ALERT_TRIGGER' in os.environ.keys() and os.environ["ALERT_TRIGGER"] == 'Y':
675
+ if "GSHEET_SERVICE_ACCOUNT_DEV" in os.environ.keys() and (userPassedArgs.backtestdaysago is None):
676
+ begin = time.time()
677
+ creds = os.environ.get("GSHEET_SERVICE_ACCOUNT_DEV")
678
+ OutputControls().printOutput(f"{colorText.GREEN} [+] Saving data to Google Spreadsheets now...{colorText.END}")
679
+
680
+ gClient = PKSpreadsheets(credentialDictStr=creds)
681
+ runOption = PKScanRunner.getFormattedChoices(userPassedArgs, selectedChoice)
682
+ df = saveResults.copy()
683
+ df["LastTradeDate"], df["LastTradeTime"] = getLatestTradeDateTime(stockDictPrimary)
684
+
685
+ gClient.df_to_sheet(df=df, sheetName=runOption)
686
+ OutputControls().printOutput(f"{colorText.GREEN} => Done in {round(time.time()-begin,2)}s{colorText.END}")
687
+
688
+ def handle_pinned_menu(self, saveResults, menuOption):
689
+ """Handle the pinned menu display and options"""
690
+ if ("RUNNER" not in os.environ.keys() and
691
+ not self.app_state.testing and
692
+ (userPassedArgs is None or
693
+ (userPassedArgs is not None and
694
+ (userPassedArgs.user is None or str(userPassedArgs.user) == DEV_CHANNEL_ID) and
695
+ (userPassedArgs.answerdefault is None or userPassedArgs.systemlaunched))) and
696
+ not userPassedArgs.testbuild and
697
+ userPassedArgs.monitor is None):
698
+
699
+ prevOutput_results = saveResults.index if (saveResults is not None and not saveResults.empty) else []
700
+ isNotPiped = (("|" not in userPassedArgs.options) if (userPassedArgs is not None and userPassedArgs.options is not None) else True)
701
+ hasFoundStocks = len(prevOutput_results) > 0 and isNotPiped
702
+
703
+ if hasFoundStocks or (self.configManager.showPinnedMenuEvenForNoResult and isNotPiped):
704
+ # ... handle pinned menu logic
705
+ pass
706
+
707
+
708
+ # Replace the original main function with the refactored version
709
+ def main(userArgs=None, optionalFinalOutcome_df=None):
710
+ """
711
+ Main entry point - replaces the original massive main function
712
+
713
+ Args:
714
+ userArgs: Command line arguments passed by user
715
+ optionalFinalOutcome_df: Optional dataframe for final results
716
+
717
+ Returns:
718
+ Tuple of screen results and save results
719
+ """
720
+ app = MainApplication(userArgs)
721
+ return app.main(userArgs, optionalFinalOutcome_df)