omniverse1 commited on
Commit
ed302b6
·
verified ·
1 Parent(s): 55613a4

update utils-1.5

Browse files
Files changed (1) hide show
  1. utils.py +204 -11
utils.py CHANGED
@@ -14,12 +14,205 @@ from dataclasses import dataclass
14
  # Import configurations
15
  from config import IDX_STOCKS, IDX_MARKET_CONFIG
16
 
17
- # ... (MarketStatusManager dan fungsi utilitas lainnya SAMA) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
  # --- Utility Functions (Modified) ---
20
 
21
  def retry_yfinance_request(func, max_retries=3, initial_delay=1):
22
- # ... (Fungsi ini SAMA) ...
23
  for attempt in range(max_retries):
24
  try:
25
  result = func()
@@ -39,7 +232,7 @@ def retry_yfinance_request(func, max_retries=3, initial_delay=1):
39
  return None
40
 
41
  def calculate_rsi(prices: pd.Series, period: int = 14) -> pd.Series:
42
- # ... (Fungsi ini SAMA) ...
43
  prices = prices.ffill().bfill()
44
  delta = prices.diff()
45
  gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
@@ -48,7 +241,7 @@ def calculate_rsi(prices: pd.Series, period: int = 14) -> pd.Series:
48
  return 100 - (100 / (1 + rs))
49
 
50
  def calculate_macd(prices: pd.Series, fast: int = 12, slow: int = 26, signal: int = 9) -> Tuple[pd.Series, pd.Series]:
51
- # ... (Fungsi ini SAMA) ...
52
  prices = prices.ffill().bfill()
53
  exp1 = prices.ewm(span=fast, adjust=False).mean()
54
  exp2 = prices.ewm(span=slow, adjust=False).mean()
@@ -57,7 +250,7 @@ def calculate_macd(prices: pd.Series, fast: int = 12, slow: int = 26, signal: in
57
  return macd, signal_line
58
 
59
  def calculate_bollinger_bands(prices: pd.Series, period: int = 20, std_dev: int = 2) -> Tuple[pd.Series, pd.Series, pd.Series]:
60
- # ... (Fungsi ini SAMA) ...
61
  prices = prices.ffill().bfill()
62
  middle_band = prices.rolling(window=period).mean()
63
  std = prices.rolling(window=period).std()
@@ -66,7 +259,7 @@ def calculate_bollinger_bands(prices: pd.Series, period: int = 20, std_dev: int
66
  return upper_band, middle_band, lower_band
67
 
68
  def calculate_technical_indicators(df: pd.DataFrame) -> pd.DataFrame:
69
- # ... (Fungsi ini SAMA) ...
70
  try:
71
  # Calculate moving averages
72
  df['SMA_20'] = df['Close'].rolling(window=20, min_periods=1).mean()
@@ -97,11 +290,11 @@ def calculate_technical_indicators(df: pd.DataFrame) -> pd.DataFrame:
97
 
98
 
99
  # --- Existing Utility Functions (Modified) ---
 
100
  # Hapus fungsi get_idx_stocks
101
 
102
  def fetch_stock_data(symbol, period_days):
103
- """Fetch stock data from Yahoo Finance with retry and calculate indicators"""
104
- # ... (Fungsi ini SAMA) ...
105
  try:
106
  end_date = datetime.now()
107
  start_date = end_date - timedelta(days=period_days + 30)
@@ -134,7 +327,7 @@ def fetch_stock_data(symbol, period_days):
134
  return None
135
 
136
  def prepare_timesfm_data(stock_data, use_volume=False):
137
- # ... (Fungsi ini SAMA) ...
138
  data = stock_data['Close'].copy()
139
 
140
  data = data.fillna(method='ffill').fillna(method='bfill')
@@ -147,7 +340,7 @@ def prepare_timesfm_data(stock_data, use_volume=False):
147
 
148
 
149
  def create_forecast_plot(historical_data, forecast_dates, forecast_prices, symbol):
150
- # ... (Fungsi ini SAMA) ...
151
  fig = make_subplots(
152
  rows=2, cols=1,
153
  shared_xaxes=True,
@@ -241,7 +434,7 @@ def create_forecast_plot(historical_data, forecast_dates, forecast_prices, symbo
241
  return fig
242
 
243
  def get_stock_info(symbol):
244
- """Get additional stock information with retry"""
245
  try:
246
  ticker = yf.Ticker(symbol)
247
  info = retry_yfinance_request(lambda: ticker.info)
 
14
  # Import configurations
15
  from config import IDX_STOCKS, IDX_MARKET_CONFIG
16
 
17
+
18
+ # --- Market Status Manager ---
19
+
20
+ @dataclass
21
+ class MarketStatus:
22
+ """Data class to hold market status information"""
23
+ is_open: bool
24
+ status_text: str
25
+ next_trading_day: str
26
+ last_updated: str
27
+ time_until_open: str
28
+ time_until_close: str
29
+ current_time_et: str
30
+ market_name: str
31
+ market_type: str
32
+ market_symbol: str
33
+
34
+ # Global configuration for market status updates
35
+ MARKET_STATUS_UPDATE_INTERVAL_MINUTES = 10 # Update every 10 minutes
36
+
37
+ class MarketStatusManager:
38
+ """Manages market status with periodic updates for the IDX market"""
39
+
40
+ def __init__(self):
41
+ self.update_interval = MARKET_STATUS_UPDATE_INTERVAL_MINUTES * 60 # Convert to seconds
42
+ self._statuses = {}
43
+ self._lock = threading.Lock()
44
+ self._stop_event = threading.Event()
45
+ self._update_thread = None
46
+ self._start_update_thread()
47
+
48
+ def _get_current_market_status(self, market_key: str = 'IDX_STOCKS') -> MarketStatus:
49
+ """Get current market status with detailed information for specific market"""
50
+ config = IDX_MARKET_CONFIG[market_key]
51
+ now = datetime.now(pytz.timezone('Asia/Jakarta')) # Use timezone-aware datetime for IDX
52
+
53
+ market_tz = pytz.timezone(config['timezone'])
54
+ market_time = now.astimezone(market_tz)
55
+
56
+ open_hour, open_minute = map(int, config['open_time'].split(':'))
57
+ close_hour, close_minute = map(int, config['close_time'].split(':'))
58
+
59
+ market_open_time = market_time.replace(hour=open_hour, minute=open_minute, second=0, microsecond=0)
60
+ market_close_time = market_time.replace(hour=close_hour, minute=close_minute, second=0, microsecond=0)
61
+
62
+ is_trading_day = market_time.weekday() in config['days']
63
+
64
+ if not is_trading_day:
65
+ is_open = False
66
+ status_text = f"{config['name']} is closed (Weekend)"
67
+ elif market_open_time <= market_time <= market_close_time:
68
+ is_open = True
69
+ status_text = f"{config['name']} is currently open"
70
+ else:
71
+ is_open = False
72
+ if market_time < market_open_time:
73
+ status_text = f"{config['name']} is closed (Before opening)"
74
+ else:
75
+ status_text = f"{config['name']} is closed (After closing)"
76
+
77
+ next_trading_day = self._get_next_trading_day(market_time, config['days'])
78
+ time_until_open = self._get_time_until_open(market_time, market_open_time, config)
79
+ time_until_close = self._get_time_until_close(market_time, market_close_time, config)
80
+
81
+ return MarketStatus(
82
+ is_open=is_open,
83
+ status_text=status_text,
84
+ next_trading_day=next_trading_day.strftime('%Y-%m-%d'),
85
+ last_updated=market_time.strftime('%Y-%m-%d %H:%M:%S %Z'),
86
+ time_until_open=time_until_open,
87
+ time_until_close=time_until_close,
88
+ current_time_et=market_time.strftime('%H:%M:%S %Z'),
89
+ market_name=config['name'],
90
+ market_type=config['type'],
91
+ market_symbol=config['symbol']
92
+ )
93
+
94
+ def _get_next_trading_day(self, current_time: datetime, trading_days: list) -> datetime:
95
+ """Get the next trading day for a specific market"""
96
+ next_day = current_time.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
97
+
98
+ while next_day.weekday() not in trading_days:
99
+ next_day += timedelta(days=1)
100
+
101
+ return next_day
102
+
103
+ def _get_time_until_open(self, current_time: datetime, market_open_time: datetime, config: dict) -> str:
104
+ """Calculate time until market opens"""
105
+ if current_time.weekday() not in config['days']:
106
+ days_until_next = 1
107
+ while (current_time + timedelta(days=days_until_next)).weekday() not in config['days']:
108
+ days_until_next += 1
109
+
110
+ next_trading_day = current_time.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=days_until_next)
111
+ next_open = next_trading_day.replace(
112
+ hour=int(config['open_time'].split(':')[0]),
113
+ minute=int(config['open_time'].split(':')[1]),
114
+ second=0, microsecond=0
115
+ )
116
+ time_diff = next_open - current_time
117
+ else:
118
+ if current_time < market_open_time:
119
+ time_diff = market_open_time - current_time
120
+ else:
121
+ days_until_next = 1
122
+ while (current_time + timedelta(days=days_until_next)).weekday() not in config['days']:
123
+ days_until_next += 1
124
+
125
+ next_trading_day = current_time.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=days_until_next)
126
+ next_open = next_trading_day.replace(
127
+ hour=int(config['open_time'].split(':')[0]),
128
+ minute=int(config['open_time'].split(':')[1]),
129
+ second=0, microsecond=0
130
+ )
131
+ time_diff = next_open - current_time
132
+
133
+ return self._format_time_delta(time_diff)
134
+
135
+ def _get_time_until_close(self, current_time: datetime, market_close_time: datetime, config: dict) -> str:
136
+ """Calculate time until market closes"""
137
+ if current_time.weekday() not in config['days']:
138
+ return "N/A (Weekend)"
139
+
140
+ if current_time < market_close_time:
141
+ time_diff = market_close_time - current_time
142
+ return self._format_time_delta(time_diff)
143
+ else:
144
+ return "Market closed for today"
145
+
146
+ def _format_time_delta(self, time_diff: timedelta) -> str:
147
+ """Format timedelta into human-readable string"""
148
+ total_seconds = int(time_diff.total_seconds())
149
+ if total_seconds < 0:
150
+ return "N/A"
151
+
152
+ days = total_seconds // 86400
153
+ hours = (total_seconds % 86400) // 3600
154
+ minutes = (total_seconds % 3600) // 60
155
+
156
+ if days > 0:
157
+ return f"{days}d {hours}h {minutes}m"
158
+ elif hours > 0:
159
+ return f"{hours}h {minutes}m"
160
+ else:
161
+ return f"{minutes}m"
162
+
163
+ def _update_loop(self):
164
+ """Background thread loop for updating market status"""
165
+ while not self._stop_event.is_set():
166
+ try:
167
+ new_statuses = {}
168
+ market_key = 'IDX_STOCKS'
169
+ new_statuses[market_key] = self._get_current_market_status(market_key)
170
+
171
+ with self._lock:
172
+ self._statuses = new_statuses
173
+ time.sleep(self.update_interval)
174
+ except Exception as e:
175
+ print(f"Error updating market status: {str(e)}")
176
+ time.sleep(60)
177
+
178
+ def _start_update_thread(self):
179
+ """Start the background update thread"""
180
+ if self._update_thread is None or not self._update_thread.is_alive():
181
+ self._stop_event.clear()
182
+ self._update_thread = threading.Thread(target=self._update_loop, daemon=True)
183
+ self._update_thread.start()
184
+
185
+ def get_status(self, market_key: str = 'IDX_STOCKS') -> MarketStatus:
186
+ """Get current market status (thread-safe)"""
187
+ with self._lock:
188
+ if market_key not in self._statuses:
189
+ self._statuses[market_key] = self._get_current_market_status(market_key)
190
+ return self._statuses[market_key]
191
+
192
+ def stop(self):
193
+ """Stop the update thread"""
194
+ self._stop_event.set()
195
+ if self._update_thread and self._update_thread.is_alive():
196
+ self._update_thread.join(timeout=5)
197
+
198
+ # --- LAZY INITIALIZATION FIX ---
199
+ _market_status_manager_instance = None
200
+
201
+ def get_market_manager():
202
+ """Returns the singleton MarketStatusManager instance, initializing it if necessary."""
203
+ global _market_status_manager_instance
204
+ if _market_status_manager_instance is None:
205
+ _market_status_manager_instance = MarketStatusManager()
206
+ return _market_status_manager_instance
207
+
208
+ # Hapus baris lama: market_status_manager = MarketStatusManager()
209
+ # --- LAZY INITIALIZATION FIX ---
210
+
211
 
212
  # --- Utility Functions (Modified) ---
213
 
214
  def retry_yfinance_request(func, max_retries=3, initial_delay=1):
215
+ # ... (SAMA) ...
216
  for attempt in range(max_retries):
217
  try:
218
  result = func()
 
232
  return None
233
 
234
  def calculate_rsi(prices: pd.Series, period: int = 14) -> pd.Series:
235
+ # ... (SAMA) ...
236
  prices = prices.ffill().bfill()
237
  delta = prices.diff()
238
  gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
 
241
  return 100 - (100 / (1 + rs))
242
 
243
  def calculate_macd(prices: pd.Series, fast: int = 12, slow: int = 26, signal: int = 9) -> Tuple[pd.Series, pd.Series]:
244
+ # ... (SAMA) ...
245
  prices = prices.ffill().bfill()
246
  exp1 = prices.ewm(span=fast, adjust=False).mean()
247
  exp2 = prices.ewm(span=slow, adjust=False).mean()
 
250
  return macd, signal_line
251
 
252
  def calculate_bollinger_bands(prices: pd.Series, period: int = 20, std_dev: int = 2) -> Tuple[pd.Series, pd.Series, pd.Series]:
253
+ # ... (SAMA) ...
254
  prices = prices.ffill().bfill()
255
  middle_band = prices.rolling(window=period).mean()
256
  std = prices.rolling(window=period).std()
 
259
  return upper_band, middle_band, lower_band
260
 
261
  def calculate_technical_indicators(df: pd.DataFrame) -> pd.DataFrame:
262
+ # ... (SAMA) ...
263
  try:
264
  # Calculate moving averages
265
  df['SMA_20'] = df['Close'].rolling(window=20, min_periods=1).mean()
 
290
 
291
 
292
  # --- Existing Utility Functions (Modified) ---
293
+
294
  # Hapus fungsi get_idx_stocks
295
 
296
  def fetch_stock_data(symbol, period_days):
297
+ # ... (SAMA) ...
 
298
  try:
299
  end_date = datetime.now()
300
  start_date = end_date - timedelta(days=period_days + 30)
 
327
  return None
328
 
329
  def prepare_timesfm_data(stock_data, use_volume=False):
330
+ # ... (SAMA) ...
331
  data = stock_data['Close'].copy()
332
 
333
  data = data.fillna(method='ffill').fillna(method='bfill')
 
340
 
341
 
342
  def create_forecast_plot(historical_data, forecast_dates, forecast_prices, symbol):
343
+ # ... (SAMA) ...
344
  fig = make_subplots(
345
  rows=2, cols=1,
346
  shared_xaxes=True,
 
434
  return fig
435
 
436
  def get_stock_info(symbol):
437
+ # ... (SAMA) ...
438
  try:
439
  ticker = yf.Ticker(symbol)
440
  info = retry_yfinance_request(lambda: ticker.info)