Dmitry Beresnev commited on
Commit
46b838c
·
1 Parent(s): 47ee674

fix new tickers filtering in ticker scanner, disable live trading, etc

Browse files
.gitignore CHANGED
@@ -29,6 +29,7 @@ src/core/linear_regression/
29
  src/core/markovs_chainss/
30
  # Ignore trading documentation (local reference only)
31
  src/core/trading/docs/
 
32
  # Ignore SQLite trading database
33
  trades.db
34
  *.db
 
29
  src/core/markovs_chainss/
30
  # Ignore trading documentation (local reference only)
31
  src/core/trading/docs/
32
+ CRITICAL_IMPROVEMENTS_SUMMARY.md
33
  # Ignore SQLite trading database
34
  trades.db
35
  *.db
src/core/ticker_scanner/core_enums.py CHANGED
@@ -34,3 +34,49 @@ class GrowthCategory(Enum):
34
  STRONG = "strong" # Top 25%
35
  MODERATE = "moderate" # Top 50%
36
  SLOW = "slow" # Bottom 50%
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  STRONG = "strong" # Top 25%
35
  MODERATE = "moderate" # Top 50%
36
  SLOW = "slow" # Bottom 50%
37
+
38
+
39
+ class Timeframe(Enum):
40
+ """Historical data timeframe options"""
41
+ ONE_DAY = "1d"
42
+ FIVE_DAYS = "5d"
43
+ ONE_MONTH = "1mo"
44
+ THREE_MONTHS = "3mo"
45
+ SIX_MONTHS = "6mo"
46
+ ONE_YEAR = "1y"
47
+ TWO_YEARS = "2y"
48
+ FIVE_YEARS = "5y"
49
+ TEN_YEARS = "10y"
50
+ YTD = "ytd"
51
+ MAX = "max"
52
+
53
+
54
+ # Minimum number of data points required for each timeframe
55
+ # These values ensure sufficient historical data for reliable analysis
56
+ # Note: 1d uses 30m intervals (~13 candles/day), 5d uses 1h intervals (~32 candles/5days)
57
+ TIMEFRAME_MIN_DATA_POINTS = {
58
+ "1d": 10, # 1 day with 30m intervals (~13 candles, conservative)
59
+ "5d": 25, # 5 days with 1h intervals (~32 candles, conservative)
60
+ "1mo": 15, # ~1 month
61
+ "3mo": 40, # ~3 months
62
+ "6mo": 80, # ~6 months
63
+ "1y": 150, # ~1 year (250 trading days)
64
+ "2y": 300, # ~2 years
65
+ "5y": 750, # ~5 years
66
+ "10y": 1500, # ~10 years
67
+ "ytd": 50, # Year to date
68
+ "max": 100, # Max available (conservative minimum)
69
+ }
70
+
71
+
72
+ def get_min_data_points_for_timeframe(timeframe: str) -> int:
73
+ """
74
+ Get the minimum number of data points required for a given timeframe.
75
+
76
+ Args:
77
+ timeframe: Timeframe string (e.g., "1y", "6mo")
78
+
79
+ Returns:
80
+ Minimum number of data points required
81
+ """
82
+ return TIMEFRAME_MIN_DATA_POINTS.get(timeframe, 100)
src/core/ticker_scanner/parallel_data_downloader.py CHANGED
@@ -13,7 +13,7 @@ from concurrent.futures import ProcessPoolExecutor, as_completed
13
 
14
  import yfinance as yf
15
 
16
- from src.core.ticker_scanner.core_enums import StockExchange
17
  from src.core.ticker_scanner.tickers_provider import TickersProvider
18
  from src.telegram_bot.logger import main_logger as logger
19
  from src.core.ticker_scanner.ticker_cache import TickerCache
@@ -53,6 +53,9 @@ def fetch_prices(ticker: str, max_retries: int = MAX_RETRIES, use_cache: bool =
53
  Returns:
54
  dict {'ticker': ticker, 'prices': ndarray, 'dates': DatetimeIndex} or None if failed
55
  """
 
 
 
56
  # Set granular intervals for short periods to ensure enough data points
57
  interval = "1d" # Default to daily
58
  if period == "1d":
@@ -75,8 +78,9 @@ def fetch_prices(ticker: str, max_retries: int = MAX_RETRIES, use_cache: bool =
75
 
76
  closes = df["Close"].dropna()
77
 
78
- # Validate data
79
- if len(closes) < MIN_DATA_POINTS:
 
80
  return None
81
 
82
  # Ensure prices and dates have same length
@@ -155,27 +159,32 @@ def download_tickers_parallel(tickers: list[str], exchange: str,
155
  # Download remaining tickers
156
  all_results = cached_results.copy()
157
  all_failed = []
 
158
 
159
  if tickers_to_download:
160
  logger.info(f"Downloading {len(tickers_to_download)} tickers...")
161
  for batch_num, ticker_batch in enumerate(batch(tickers_to_download, BATCH_SIZE), start=1):
162
  logger.info(f"Processing batch {batch_num}: {len(ticker_batch)} tickers")
163
- results, failed = process_batch(ticker_batch, exchange, max_workers, period)
164
  all_results.extend(results)
165
  all_failed.extend(failed)
 
166
  # small sleep between batches to reduce rate-limit chance
167
  time.sleep(1 + random.random())
168
 
 
169
  logger.info(f"Total available: {len(all_results)} (cached: {len(cached_results)}, downloaded: {len(all_results) - len(cached_results)})")
170
  if all_failed:
171
- logger.warning(f"Total failed: {len(all_failed)} - {all_failed[:10]}") # Show first 10
 
 
172
 
173
  return all_results
174
 
175
- def process_batch(ticker_batch: list[str], exchange: str, max_workers: int, period: str = "max") -> tuple[list[dict[str, Any]], list[Any]]:
176
  """
177
  Process a batch of tickers in parallel using multiprocessing.
178
- Returns tuple (successful_results, failed_tickers)
179
 
180
  Args:
181
  ticker_batch: List of ticker symbols to process
@@ -187,6 +196,9 @@ def process_batch(ticker_batch: list[str], exchange: str, max_workers: int, peri
187
  """
188
  results = []
189
  failed = []
 
 
 
190
  with ProcessPoolExecutor(max_workers=max_workers) as executor:
191
  # Don't use cache in subprocess - already handled in main process
192
  futures = {executor.submit(fetch_prices, t, use_cache=False, period=period): t for t in ticker_batch}
@@ -195,14 +207,20 @@ def process_batch(ticker_batch: list[str], exchange: str, max_workers: int, peri
195
  try:
196
  res = future.result()
197
  if res:
198
- # Cache the result in the main process after download
199
- _cache.set(exchange, res['ticker'], res, period)
200
- results.append(res)
 
 
 
 
 
 
201
  else:
202
  failed.append(ticker)
203
  except Exception:
204
  failed.append(ticker)
205
- return results, failed
206
 
207
  def run_parallel_data_downloader(exchange: StockExchange = StockExchange.NASDAQ,
208
  limit: int = 200,
 
13
 
14
  import yfinance as yf
15
 
16
+ from src.core.ticker_scanner.core_enums import StockExchange, get_min_data_points_for_timeframe
17
  from src.core.ticker_scanner.tickers_provider import TickersProvider
18
  from src.telegram_bot.logger import main_logger as logger
19
  from src.core.ticker_scanner.ticker_cache import TickerCache
 
53
  Returns:
54
  dict {'ticker': ticker, 'prices': ndarray, 'dates': DatetimeIndex} or None if failed
55
  """
56
+ # Get minimum data points required for this timeframe
57
+ min_data_points = get_min_data_points_for_timeframe(period)
58
+
59
  # Set granular intervals for short periods to ensure enough data points
60
  interval = "1d" # Default to daily
61
  if period == "1d":
 
78
 
79
  closes = df["Close"].dropna()
80
 
81
+ # Validate minimum data points for the requested timeframe
82
+ if len(closes) < min_data_points:
83
+ logger.debug(f"{ticker}: Insufficient data for {period} timeframe. Got {len(closes)} points, need {min_data_points}")
84
  return None
85
 
86
  # Ensure prices and dates have same length
 
159
  # Download remaining tickers
160
  all_results = cached_results.copy()
161
  all_failed = []
162
+ insufficient_data = []
163
 
164
  if tickers_to_download:
165
  logger.info(f"Downloading {len(tickers_to_download)} tickers...")
166
  for batch_num, ticker_batch in enumerate(batch(tickers_to_download, BATCH_SIZE), start=1):
167
  logger.info(f"Processing batch {batch_num}: {len(ticker_batch)} tickers")
168
+ results, failed, filtered = process_batch(ticker_batch, exchange, max_workers, period)
169
  all_results.extend(results)
170
  all_failed.extend(failed)
171
+ insufficient_data.extend(filtered)
172
  # small sleep between batches to reduce rate-limit chance
173
  time.sleep(1 + random.random())
174
 
175
+ min_data_points = get_min_data_points_for_timeframe(period)
176
  logger.info(f"Total available: {len(all_results)} (cached: {len(cached_results)}, downloaded: {len(all_results) - len(cached_results)})")
177
  if all_failed:
178
+ logger.warning(f"Total failed to fetch: {len(all_failed)} - {all_failed[:10]}") # Show first 10
179
+ if insufficient_data:
180
+ logger.info(f"Filtered out (insufficient data for {period}): {len(insufficient_data)} tickers - {insufficient_data[:10]}") # Show first 10
181
 
182
  return all_results
183
 
184
+ def process_batch(ticker_batch: list[str], exchange: str, max_workers: int, period: str = "max") -> tuple[list[dict[str, Any]], list[Any], list[str]]:
185
  """
186
  Process a batch of tickers in parallel using multiprocessing.
187
+ Returns tuple (successful_results, failed_tickers, filtered_tickers)
188
 
189
  Args:
190
  ticker_batch: List of ticker symbols to process
 
196
  """
197
  results = []
198
  failed = []
199
+ filtered = []
200
+ min_data_points = get_min_data_points_for_timeframe(period)
201
+
202
  with ProcessPoolExecutor(max_workers=max_workers) as executor:
203
  # Don't use cache in subprocess - already handled in main process
204
  futures = {executor.submit(fetch_prices, t, use_cache=False, period=period): t for t in ticker_batch}
 
207
  try:
208
  res = future.result()
209
  if res:
210
+ # Check if we have sufficient data points for the timeframe
211
+ data_points = len(res['prices'])
212
+ if data_points >= min_data_points:
213
+ # Cache the result in the main process after download
214
+ _cache.set(exchange, res['ticker'], res, period)
215
+ results.append(res)
216
+ else:
217
+ # Track tickers with insufficient data
218
+ filtered.append(ticker)
219
  else:
220
  failed.append(ticker)
221
  except Exception:
222
  failed.append(ticker)
223
+ return results, failed, filtered
224
 
225
  def run_parallel_data_downloader(exchange: StockExchange = StockExchange.NASDAQ,
226
  limit: int = 200,
src/telegram_bot/telegram_bot_service.py CHANGED
@@ -234,13 +234,14 @@ class TelegramBotService:
234
  "• /scan LSE max\n"
235
  "• /scan ETF 3m 5\n"
236
  )
237
- response += "\n🤖 <b>Trading Commands:</b>\n"
238
- response += "/backtest TICKER - Backtest MACD strategy on stock (e.g., /backtest AAPL)\n"
239
- response += "/live_status - Check live trading status and open positions\n"
240
- response += "/portfolio - Get portfolio summary with P&L\n"
241
- response += "/close_all - Emergency: Close all open positions\n\n"
242
- response += "🤖 AI-powered trading insights\n"
243
- response += "🔗 Powered by OpenRouter and Gemini API\n\n"
 
244
 
245
  elif base_command == "/status":
246
  response = "✅ <b>Bot Status: Online</b>\n\n"
@@ -294,21 +295,21 @@ class TelegramBotService:
294
  text=None, user_name=user_name)
295
  return
296
 
297
- elif base_command == "/backtest":
298
- await self._handle_backtest_command(chat_id, command_parts, user_name)
299
- return
300
 
301
- elif base_command == "/live_status":
302
- await self._handle_live_status_command(chat_id, user_name)
303
- return
304
 
305
- elif base_command == "/portfolio":
306
- await self._handle_portfolio_command(chat_id, user_name)
307
- return
308
 
309
- elif base_command == "/close_all":
310
- await self._handle_close_all_command(chat_id, user_name)
311
- return
312
 
313
  else:
314
  response = f"❓ Unknown command: {command}\n\n"
@@ -849,370 +850,372 @@ class TelegramBotService:
849
  error_msg += "Please try again later."
850
  await self.send_message_via_proxy(chat_id, error_msg)
851
 
852
- async def _handle_backtest_command(
853
- self, chat_id: int, command_parts: list[str], user_name: str
854
- ) -> None:
855
- """
856
- Handle backtest command
857
- Usage: /backtest TICKER [PERIOD]
858
- Example: /backtest AAPL 6mo
859
- """
860
- if len(command_parts) < 2:
861
- await self.send_message_via_proxy(
862
- chat_id,
863
- "❌ Please specify a ticker: /backtest AAPL [period]\n\n"
864
- "Supported periods: 1mo, 3mo, 6mo, 1y, 2y, 5y, max (default: 1y)\n\n"
865
- "Examples:\n• /backtest AAPL\n• /backtest TSLA 6mo"
866
- )
867
- return
868
-
869
- ticker = command_parts[1].upper()
870
- period = command_parts[2].lower() if len(command_parts) > 2 else "1y"
871
-
872
- # Validate period
873
- valid_periods = ["1mo", "3mo", "6mo", "1y", "2y", "5y", "max"]
874
- if period not in valid_periods:
875
- await self.send_message_via_proxy(
876
- chat_id,
877
- f"❌ Invalid period: {period}\n\n"
878
- f"Supported: {', '.join(valid_periods)}"
879
- )
880
- return
881
-
882
- await self.send_message_via_proxy(
883
- chat_id,
884
- f"⏳ <b>Backtesting MACD strategy on {ticker} ({period})...</b>\n\n"
885
- f"📥 Downloading historical data...\n"
886
- f"📊 Generating trading signals...\n"
887
- f"🧮 Simulating trades...\n"
888
- f"📈 Calculating metrics..."
889
- )
890
-
891
- try:
892
- import yfinance as yf
893
- import numpy as np
894
-
895
- # Download data
896
- data = yf.download(ticker, period=period, progress=False)
897
-
898
- if data.empty or len(data) < 50:
899
- await self.send_message_via_proxy(
900
- chat_id,
901
- f"❌ Not enough historical data for {ticker}\n\n"
902
- f"Need at least 50 candles, got {len(data)}"
903
- )
904
- return
905
-
906
- # Initialize strategy and backtest
907
- strategy = AdvancedMACDStrategy()
908
- risk_engine = RiskEngine(initial_capital=10000, max_risk_per_trade=0.02)
909
- backtest = VectorizedBacktest(
910
- strategy=strategy,
911
- risk_engine=risk_engine,
912
- initial_capital=10000,
913
- commission=0.001
914
- )
915
-
916
- # Run backtest
917
- trades, equity_curve, metrics = backtest.run(data, ticker)
918
-
919
- # Format results
920
- result_text = self._format_backtest_results(ticker, trades, equity_curve, metrics, period)
921
- await self.send_long_message(chat_id, result_text)
922
-
923
- except ImportError:
924
- await self.send_message_via_proxy(
925
- chat_id,
926
- "❌ Required library not installed: yfinance\n\n"
927
- "Please install: pip install yfinance"
928
- )
929
- except Exception as e:
930
- logger.error(f"Error in backtest command: {e}", exc_info=True)
931
- await self.send_message_via_proxy(
932
- chat_id,
933
- f"❌ Backtest failed for {ticker}: {str(e)}"
934
- )
935
-
936
- async def _handle_live_status_command(
937
- self, chat_id: int, user_name: str
938
- ) -> None:
939
- """
940
- Handle live_status command
941
- Shows current trading status and open positions
942
- """
943
- if not self.live_trader or not self.live_trader.is_running:
944
- await self.send_message_via_proxy(
945
- chat_id,
946
- "⛔ <b>Live trading is not active</b>\n\n"
947
- "Status: Offline\n\n"
948
- "To start live trading:\n"
949
- "1. Ensure Alpaca API keys are configured\n"
950
- "2. Use `/live_start AAPL NVDA TSLA` to start trading symbols\n"
951
- "3. Monitor trades with `/live_status` and `/portfolio`"
952
- )
953
- return
954
-
955
- try:
956
- status = self.live_trader.get_status()
957
- account = self.live_trader.broker.get_account()
958
- positions = self.live_trader.broker.get_positions()
959
-
960
- response = "🟢 <b>Live Trading Status: ACTIVE</b>\n\n"
961
- response += f"💰 <b>Account:</b>\n"
962
- response += f" Equity: ${account.equity:,.2f}\n"
963
- response += f" Cash: ${account.cash:,.2f}\n"
964
- response += f" Buying Power: ${account.buying_power:,.2f}\n\n"
965
-
966
- response += f"📊 <b>Trading:</b>\n"
967
- response += f" Symbols: {', '.join(status['symbols'])}\n"
968
- response += f" Approval Mode: {'ON' if status['approval_mode'] else 'OFF'}\n"
969
- response += f" Pending Approvals: {status['pending_approvals']}\n"
970
- response += f" Open Positions: {status['open_positions']}\n\n"
971
-
972
- response += f"📈 <b>Performance:</b>\n"
973
- response += f" Executed Signals: {status['executed_signals']}\n"
974
- response += f" Skipped Signals: {status['skipped_signals']}\n\n"
975
-
976
- if positions:
977
- response += "📍 <b>Open Positions:</b>\n"
978
- for pos in positions:
979
- response += f" {pos.symbol}: {pos.quantity} @ ${pos.entry_price:.2f}\n"
980
- response += f" Current: ${pos.current_price:.2f} | "
981
- response += f"P&L: ${pos.unrealized_pnl:.2f} ({pos.unrealized_pnl_pct:.1f}%)\n"
982
-
983
- await self.send_message_via_proxy(chat_id, response)
984
-
985
- except Exception as e:
986
- logger.error(f"Error getting live status: {e}", exc_info=True)
987
- await self.send_message_via_proxy(
988
- chat_id,
989
- f"❌ Error retrieving live status: {str(e)}"
990
- )
991
-
992
- async def _handle_portfolio_command(
993
- self, chat_id: int, user_name: str
994
- ) -> None:
995
- """
996
- Handle portfolio command
997
- Shows detailed portfolio summary with P&L
998
- """
999
- if not self.live_trader or not self.live_trader.is_running:
1000
- await self.send_message_via_proxy(
1001
- chat_id,
1002
- "⛔ <b>Live trading is not active</b>\n\n"
1003
- "Cannot retrieve portfolio. Start live trading first."
1004
- )
1005
- return
1006
-
1007
- try:
1008
- summary = self.live_trader.order_manager.get_open_trades_summary()
1009
- execution_history = self.live_trader.order_manager.get_execution_history(limit=10)
1010
-
1011
- response = "💼 <b>PORTFOLIO SUMMARY</b>\n"
1012
- response += "━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
1013
-
1014
- response += f"💰 <b>Account Overview:</b>\n"
1015
- response += f" Total Equity: ${summary['total_equity']:,.2f}\n"
1016
- response += f" Cash Available: ${summary['total_cash']:,.2f}\n"
1017
- response += f" Unrealized P&L: ${summary['total_unrealized_pnl']:,.2f}\n"
1018
- response += f" Portfolio Heat: {summary['portfolio_heat']*100:.2f}%\n"
1019
- response += f" Open Positions: {summary['total_positions']}\n\n"
1020
-
1021
- if summary['positions']:
1022
- response += "📍 <b>Open Positions:</b>\n"
1023
- for pos in summary['positions']:
1024
- emoji = "📈" if pos['unrealized_pnl'] > 0 else "📉"
1025
- response += f" {emoji} {pos['symbol']}\n"
1026
- response += f" Qty: {pos['quantity']} @ ${pos['entry_price']:.2f}\n"
1027
- response += f" P&L: ${pos['unrealized_pnl']:.2f} ({pos['unrealized_pnl_pct']:+.1f}%)\n"
1028
- else:
1029
- response += "No open positions\n\n"
1030
-
1031
- if execution_history:
1032
- response += "\n📊 <b>Recent Executions (last 10):</b>\n"
1033
- for report in execution_history[:5]: # Show last 5 to fit in message
1034
- status_emoji = "✅" if report.status == "filled" else "❌"
1035
- response += f" {status_emoji} {report.symbol} {report.side.upper()}\n"
1036
- response += f" {report.status.upper()} @ ${report.entry_price:.2f}\n"
1037
-
1038
- await self.send_message_via_proxy(chat_id, response)
1039
-
1040
- except Exception as e:
1041
- logger.error(f"Error getting portfolio: {e}", exc_info=True)
1042
- await self.send_message_via_proxy(
1043
- chat_id,
1044
- f"❌ Error retrieving portfolio: {str(e)}"
1045
- )
1046
-
1047
- async def _handle_close_all_command(
1048
- self, chat_id: int, user_name: str
1049
- ) -> None:
1050
- """
1051
- Handle close_all command
1052
- Emergency: Close all open positions
1053
- """
1054
- if not self.live_trader or not self.live_trader.is_running:
1055
- await self.send_message_via_proxy(
1056
- chat_id,
1057
- "⛔ <b>Live trading is not active</b>\n\n"
1058
- "No positions to close."
1059
- )
1060
- return
1061
-
1062
- try:
1063
- positions = self.live_trader.broker.get_positions()
1064
-
1065
- if not positions:
1066
- await self.send_message_via_proxy(
1067
- chat_id,
1068
- "✅ <b>No open positions to close</b>\n\n"
1069
- "Portfolio is flat."
1070
- )
1071
- return
1072
-
1073
- await self.send_message_via_proxy(
1074
- chat_id,
1075
- "⚠️ <b>CLOSING ALL POSITIONS...</b>\n\n"
1076
- f"Closing {len(positions)} position(s):\n" +
1077
- "\n".join([f" {p.symbol}: {p.quantity} shares" for p in positions])
1078
- )
1079
-
1080
- # Close all positions
1081
- closed_orders = self.live_trader.order_manager.close_all()
1082
-
1083
- response = "✅ <b>ALL POSITIONS CLOSED</b>\n\n"
1084
- response += f"Closed {len(closed_orders)} position(s):\n"
1085
- for order in closed_orders:
1086
- response += f" ✓ {order.symbol}: {order.quantity} shares\n"
1087
- response += f"\n⏰ Time: {datetime.now().strftime('%H:%M:%S')}"
1088
-
1089
- await self.send_message_via_proxy(chat_id, response)
1090
- logger.info(f"All positions closed via Telegram command by {user_name}")
1091
-
1092
- except Exception as e:
1093
- logger.error(f"Error closing all positions: {e}", exc_info=True)
1094
- await self.send_message_via_proxy(
1095
- chat_id,
1096
- f"❌ Error closing positions: {str(e)}"
1097
- )
1098
-
1099
- def _format_backtest_results(
1100
- self, ticker: str, trades: list, equity_curve: list, metrics: dict, period: str
1101
- ) -> str:
1102
- """Format backtest results for Telegram"""
1103
- try:
1104
- if not trades or len(trades) == 0:
1105
- return (
1106
- f"📊 <b>Backtest Results: {ticker} ({period})</b>\n\n"
1107
- f"⚠️ No trades generated\n"
1108
- f"The MACD strategy did not generate any signals on this timeframe."
1109
- )
1110
-
1111
- # Extract metrics
1112
- win_rate = metrics.get('win_rate', 0)
1113
- total_return = metrics.get('total_return', 0)
1114
- sharpe = metrics.get('sharpe_ratio', 0)
1115
- max_dd = metrics.get('max_drawdown', 0)
1116
- profit_factor = metrics.get('profit_factor', 0)
1117
- trades_count = metrics.get('total_trades', 0)
1118
-
1119
- # Emojis based on metrics
1120
- return_emoji = "📈" if total_return > 0 else "📉"
1121
- wr_emoji = "🟢" if win_rate > 0.5 else "🔴" if win_rate < 0.3 else "🟡"
1122
- sharpe_emoji = "🟢" if sharpe > 1.0 else "🟡" if sharpe > 0 else "🔴"
1123
-
1124
- result = f"""
1125
- 📊 <b>Backtest Results: {ticker}</b>
1126
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1127
- Period: {period}
1128
- 📉 Data Points: {len(equity_curve)} candles
1129
-
1130
- 📈 <b>Performance Metrics:</b>
1131
- {return_emoji} Total Return: {total_return:+.2f}%
1132
- 🎯 Win Rate: {win_rate*100:.1f}% {wr_emoji}
1133
- 📊 Profit Factor: {profit_factor:.2f}
1134
- 💹 Sharpe Ratio: {sharpe:.2f} {sharpe_emoji}
1135
- 📉 Max Drawdown: {max_dd*100:.1f}%
1136
-
1137
- 📋 <b>Trade Statistics:</b>
1138
- 🔔 Total Trades: {trades_count}
1139
- Winning Trades: {sum(1 for t in trades if t.get('pnl', 0) > 0)}
1140
- Losing Trades: {sum(1 for t in trades if t.get('pnl', 0) < 0)}
1141
-
1142
- 💰 <b>Trade Summary (first 5):</b>
1143
- """
1144
- for i, trade in enumerate(trades[:5]):
1145
- side = "BUY" if trade.get('side') == 'buy' else "SELL"
1146
- entry = trade.get('entry_price', 0)
1147
- exit_p = trade.get('exit_price', 0)
1148
- pnl = trade.get('pnl', 0)
1149
- emoji = "✅" if pnl > 0 else "❌"
1150
- result += f"{emoji} {i+1}. {side} @ ${entry:.2f} → ${exit_p:.2f} | P&L: ${pnl:+.2f}\n"
1151
-
1152
- result += f"\n⚠️ <b>Disclaimer:</b>\n"
1153
- result += f"Backtesting results are based on historical data.\n"
1154
- result += f"Past performance does not guarantee future results.\n"
1155
- result += f"Use as analysis tool only, not trading advice."
1156
-
1157
- return result
1158
-
1159
- except Exception as e:
1160
- logger.error(f"Error formatting backtest results: {e}")
1161
- return f"❌ Error formatting backtest results: {str(e)}"
1162
-
1163
- async def process_approval_callback(self, approval_id: str, action: str, chat_id: int) -> None:
1164
- """
1165
- Process approval callback from Telegram buttons
1166
- Called when user clicks approve/reject button
1167
-
1168
- Args:
1169
- approval_id: ID of the approval to process
1170
- action: 'approve' or 'reject'
1171
- chat_id: Chat ID for sending confirmation
1172
- """
1173
- try:
1174
- if not self.live_trader:
1175
- await self.send_message_via_proxy(
1176
- chat_id,
1177
- "❌ Live trader not initialized"
1178
- )
1179
- return
1180
-
1181
- if action.lower() == "approve":
1182
- result = await self.live_trader.approve_signal(approval_id)
1183
- if result:
1184
- await self.send_message_via_proxy(
1185
- chat_id,
1186
- f"✅ <b>Signal Approved</b>\n\n"
1187
- f"Approval ID: {approval_id}\n"
1188
- f"Status: Executing order..."
1189
- )
1190
- logger.info(f"Signal {approval_id} approved by user")
1191
- else:
1192
- await self.send_message_via_proxy(
1193
- chat_id,
1194
- f"❌ Approval {approval_id} not found or already processed"
1195
- )
1196
-
1197
- elif action.lower() == "reject":
1198
- result = await self.live_trader.reject_signal(approval_id)
1199
- if result:
1200
- await self.send_message_via_proxy(
1201
- chat_id,
1202
- f"❌ <b>Signal Rejected</b>\n\n"
1203
- f"Approval ID: {approval_id}\n"
1204
- f"Status: Signal skipped"
1205
- )
1206
- logger.info(f"Signal {approval_id} rejected by user")
1207
- else:
1208
- await self.send_message_via_proxy(
1209
- chat_id,
1210
- f"❌ Approval {approval_id} not found or already processed"
1211
- )
1212
-
1213
- except Exception as e:
1214
- logger.error(f"Error processing approval callback: {e}", exc_info=True)
1215
- await self.send_message_via_proxy(
1216
- chat_id,
1217
- f"❌ Error processing approval: {str(e)}"
1218
- )
 
 
 
234
  "• /scan LSE max\n"
235
  "• /scan ETF 3m 5\n"
236
  )
237
+ # Trading commands temporarily disabled
238
+ # response += "\n🤖 <b>Trading Commands:</b>\n"
239
+ # response += "/backtest TICKER - Backtest MACD strategy on stock (e.g., /backtest AAPL)\n"
240
+ # response += "/live_status - Check live trading status and open positions\n"
241
+ # response += "/portfolio - Get portfolio summary with P&L\n"
242
+ # response += "/close_all - Emergency: Close all open positions\n\n"
243
+ # response += "🤖 AI-powered trading insights\n"
244
+ # response += "🔗 Powered by OpenRouter and Gemini API\n\n"
245
 
246
  elif base_command == "/status":
247
  response = "✅ <b>Bot Status: Online</b>\n\n"
 
295
  text=None, user_name=user_name)
296
  return
297
 
298
+ # elif base_command == "/backtest":
299
+ # await self._handle_backtest_command(chat_id, command_parts, user_name)
300
+ # return
301
 
302
+ # elif base_command == "/live_status":
303
+ # await self._handle_live_status_command(chat_id, user_name)
304
+ # return
305
 
306
+ # elif base_command == "/portfolio":
307
+ # await self._handle_portfolio_command(chat_id, user_name)
308
+ # return
309
 
310
+ # elif base_command == "/close_all":
311
+ # await self._handle_close_all_command(chat_id, user_name)
312
+ # return
313
 
314
  else:
315
  response = f"❓ Unknown command: {command}\n\n"
 
850
  error_msg += "Please try again later."
851
  await self.send_message_via_proxy(chat_id, error_msg)
852
 
853
+ # # Trading commands temporarily disabled for live trading maintenance
854
+ # async def _handle_backtest_command(
855
+ # self, chat_id: int, command_parts: list[str], user_name: str
856
+ # ) -> None:
857
+ # """
858
+ # Handle backtest command
859
+ # Usage: /backtest TICKER [PERIOD]
860
+ # Example: /backtest AAPL 6mo
861
+ # """
862
+ # if len(command_parts) < 2:
863
+ # await self.send_message_via_proxy(
864
+ # chat_id,
865
+ # " Please specify a ticker: /backtest AAPL [period]\n\n"
866
+ # "Supported periods: 1mo, 3mo, 6mo, 1y, 2y, 5y, max (default: 1y)\n\n"
867
+ # "Examples:\n• /backtest AAPL\n• /backtest TSLA 6mo"
868
+ # )
869
+ # return
870
+ #
871
+ # ticker = command_parts[1].upper()
872
+ # period = command_parts[2].lower() if len(command_parts) > 2 else "1y"
873
+ #
874
+ # # Validate period
875
+ # valid_periods = ["1mo", "3mo", "6mo", "1y", "2y", "5y", "max"]
876
+ # if period not in valid_periods:
877
+ # await self.send_message_via_proxy(
878
+ # chat_id,
879
+ # f"❌ Invalid period: {period}\n\n"
880
+ # f"Supported: {', '.join(valid_periods)}"
881
+ # )
882
+ # return
883
+ #
884
+ # await self.send_message_via_proxy(
885
+ # chat_id,
886
+ # f" <b>Backtesting MACD strategy on {ticker} ({period})...</b>\n\n"
887
+ # f"📥 Downloading historical data...\n"
888
+ # f"📊 Generating trading signals...\n"
889
+ # f"🧮 Simulating trades...\n"
890
+ # f"📈 Calculating metrics..."
891
+ # )
892
+ #
893
+ # try:
894
+ # import yfinance as yf
895
+ # import numpy as np
896
+ #
897
+ # # Download data
898
+ # data = yf.download(ticker, period=period, progress=False)
899
+ #
900
+ # if data.empty or len(data) < 50:
901
+ # await self.send_message_via_proxy(
902
+ # chat_id,
903
+ # f" Not enough historical data for {ticker}\n\n"
904
+ # f"Need at least 50 candles, got {len(data)}"
905
+ # )
906
+ # return
907
+ #
908
+ # # Initialize strategy and backtest
909
+ # strategy = AdvancedMACDStrategy()
910
+ # risk_engine = RiskEngine(initial_capital=10000, max_risk_per_trade=0.02)
911
+ # backtest = VectorizedBacktest(
912
+ # strategy=strategy,
913
+ # risk_engine=risk_engine,
914
+ # initial_capital=10000,
915
+ # commission=0.001
916
+ # )
917
+ #
918
+ # # Run backtest
919
+ # trades, equity_curve, metrics = backtest.run(data, ticker)
920
+ #
921
+ # # Format results
922
+ # result_text = self._format_backtest_results(ticker, trades, equity_curve, metrics, period)
923
+ # await self.send_long_message(chat_id, result_text)
924
+ #
925
+ # except ImportError:
926
+ # await self.send_message_via_proxy(
927
+ # chat_id,
928
+ # " Required library not installed: yfinance\n\n"
929
+ # "Please install: pip install yfinance"
930
+ # )
931
+ # except Exception as e:
932
+ # logger.error(f"Error in backtest command: {e}", exc_info=True)
933
+ # await self.send_message_via_proxy(
934
+ # chat_id,
935
+ # f"❌ Backtest failed for {ticker}: {str(e)}"
936
+ # )
937
+
938
+ # async def _handle_live_status_command(
939
+ # self, chat_id: int, user_name: str
940
+ # ) -> None:
941
+ # """
942
+ # Handle live_status command
943
+ # Shows current trading status and open positions
944
+ # """
945
+ # if not self.live_trader or not self.live_trader.is_running:
946
+ # await self.send_message_via_proxy(
947
+ # chat_id,
948
+ # " <b>Live trading is not active</b>\n\n"
949
+ # "Status: Offline\n\n"
950
+ # "To start live trading:\n"
951
+ # "1. Ensure Alpaca API keys are configured\n"
952
+ # "2. Use `/live_start AAPL NVDA TSLA` to start trading symbols\n"
953
+ # "3. Monitor trades with `/live_status` and `/portfolio`"
954
+ # )
955
+ # return
956
+ #
957
+ # try:
958
+ # status = self.live_trader.get_status()
959
+ # account = self.live_trader.broker.get_account()
960
+ # positions = self.live_trader.broker.get_positions()
961
+ #
962
+ # response = "🟢 <b>Live Trading Status: ACTIVE</b>\n\n"
963
+ # response += f"💰 <b>Account:</b>\n"
964
+ # response += f" Equity: ${account.equity:,.2f}\n"
965
+ # response += f" Cash: ${account.cash:,.2f}\n"
966
+ # response += f" Buying Power: ${account.buying_power:,.2f}\n\n"
967
+ #
968
+ # response += f"📊 <b>Trading:</b>\n"
969
+ # response += f" Symbols: {', '.join(status['symbols'])}\n"
970
+ # response += f" Approval Mode: {'ON' if status['approval_mode'] else 'OFF'}\n"
971
+ # response += f" Pending Approvals: {status['pending_approvals']}\n"
972
+ # response += f" Open Positions: {status['open_positions']}\n\n"
973
+ #
974
+ # response += f"📈 <b>Performance:</b>\n"
975
+ # response += f" Executed Signals: {status['executed_signals']}\n"
976
+ # response += f" Skipped Signals: {status['skipped_signals']}\n\n"
977
+ #
978
+ # if positions:
979
+ # response += "📍 <b>Open Positions:</b>\n"
980
+ # for pos in positions:
981
+ # response += f" {pos.symbol}: {pos.quantity} @ ${pos.entry_price:.2f}\n"
982
+ # response += f" Current: ${pos.current_price:.2f} | "
983
+ # response += f"P&L: ${pos.unrealized_pnl:.2f} ({pos.unrealized_pnl_pct:.1f}%)\n"
984
+ #
985
+ # await self.send_message_via_proxy(chat_id, response)
986
+ #
987
+ # except Exception as e:
988
+ # logger.error(f"Error getting live status: {e}", exc_info=True)
989
+ # await self.send_message_via_proxy(
990
+ # chat_id,
991
+ # f"❌ Error retrieving live status: {str(e)}"
992
+ # )
993
+
994
+ # async def _handle_portfolio_command(
995
+ # self, chat_id: int, user_name: str
996
+ # ) -> None:
997
+ # """
998
+ # Handle portfolio command
999
+ # Shows detailed portfolio summary with P&L
1000
+ # """
1001
+ # if not self.live_trader or not self.live_trader.is_running:
1002
+ # await self.send_message_via_proxy(
1003
+ # chat_id,
1004
+ # " <b>Live trading is not active</b>\n\n"
1005
+ # "Cannot retrieve portfolio. Start live trading first."
1006
+ # )
1007
+ # return
1008
+ #
1009
+ # try:
1010
+ # summary = self.live_trader.order_manager.get_open_trades_summary()
1011
+ # execution_history = self.live_trader.order_manager.get_execution_history(limit=10)
1012
+ #
1013
+ # response = "💼 <b>PORTFOLIO SUMMARY</b>\n"
1014
+ # response += "━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
1015
+ #
1016
+ # response += f"💰 <b>Account Overview:</b>\n"
1017
+ # response += f" Total Equity: ${summary['total_equity']:,.2f}\n"
1018
+ # response += f" Cash Available: ${summary['total_cash']:,.2f}\n"
1019
+ # response += f" Unrealized P&L: ${summary['total_unrealized_pnl']:,.2f}\n"
1020
+ # response += f" Portfolio Heat: {summary['portfolio_heat']*100:.2f}%\n"
1021
+ # response += f" Open Positions: {summary['total_positions']}\n\n"
1022
+ #
1023
+ # if summary['positions']:
1024
+ # response += "📍 <b>Open Positions:</b>\n"
1025
+ # for pos in summary['positions']:
1026
+ # emoji = "📈" if pos['unrealized_pnl'] > 0 else "📉"
1027
+ # response += f" {emoji} {pos['symbol']}\n"
1028
+ # response += f" Qty: {pos['quantity']} @ ${pos['entry_price']:.2f}\n"
1029
+ # response += f" P&L: ${pos['unrealized_pnl']:.2f} ({pos['unrealized_pnl_pct']:+.1f}%)\n"
1030
+ # else:
1031
+ # response += "No open positions\n\n"
1032
+ #
1033
+ # if execution_history:
1034
+ # response += "\n📊 <b>Recent Executions (last 10):</b>\n"
1035
+ # for report in execution_history[:5]: # Show last 5 to fit in message
1036
+ # status_emoji = "✅" if report.status == "filled" else "❌"
1037
+ # response += f" {status_emoji} {report.symbol} {report.side.upper()}\n"
1038
+ # response += f" {report.status.upper()} @ ${report.entry_price:.2f}\n"
1039
+ #
1040
+ # await self.send_message_via_proxy(chat_id, response)
1041
+ #
1042
+ # except Exception as e:
1043
+ # logger.error(f"Error getting portfolio: {e}", exc_info=True)
1044
+ # await self.send_message_via_proxy(
1045
+ # chat_id,
1046
+ # f"❌ Error retrieving portfolio: {str(e)}"
1047
+ # )
1048
+
1049
+ # async def _handle_close_all_command(
1050
+ # self, chat_id: int, user_name: str
1051
+ # ) -> None:
1052
+ # """
1053
+ # Handle close_all command
1054
+ # Emergency: Close all open positions
1055
+ # """
1056
+ # if not self.live_trader or not self.live_trader.is_running:
1057
+ # await self.send_message_via_proxy(
1058
+ # chat_id,
1059
+ # " <b>Live trading is not active</b>\n\n"
1060
+ # "No positions to close."
1061
+ # )
1062
+ # return
1063
+ #
1064
+ # try:
1065
+ # positions = self.live_trader.broker.get_positions()
1066
+ #
1067
+ # if not positions:
1068
+ # await self.send_message_via_proxy(
1069
+ # chat_id,
1070
+ # " <b>No open positions to close</b>\n\n"
1071
+ # "Portfolio is flat."
1072
+ # )
1073
+ # return
1074
+ #
1075
+ # await self.send_message_via_proxy(
1076
+ # chat_id,
1077
+ # "⚠️ <b>CLOSING ALL POSITIONS...</b>\n\n"
1078
+ # f"Closing {len(positions)} position(s):\n" +
1079
+ # "\n".join([f" • {p.symbol}: {p.quantity} shares" for p in positions])
1080
+ # )
1081
+ #
1082
+ # # Close all positions
1083
+ # closed_orders = self.live_trader.order_manager.close_all()
1084
+ #
1085
+ # response = " <b>ALL POSITIONS CLOSED</b>\n\n"
1086
+ # response += f"Closed {len(closed_orders)} position(s):\n"
1087
+ # for order in closed_orders:
1088
+ # response += f" {order.symbol}: {order.quantity} shares\n"
1089
+ # response += f"\n⏰ Time: {datetime.now().strftime('%H:%M:%S')}"
1090
+ #
1091
+ # await self.send_message_via_proxy(chat_id, response)
1092
+ # logger.info(f"All positions closed via Telegram command by {user_name}")
1093
+ #
1094
+ # except Exception as e:
1095
+ # logger.error(f"Error closing all positions: {e}", exc_info=True)
1096
+ # await self.send_message_via_proxy(
1097
+ # chat_id,
1098
+ # f"❌ Error closing positions: {str(e)}"
1099
+ # )
1100
+
1101
+ # # Trading feature disabled - commented out for maintenance
1102
+ # def _format_backtest_results(
1103
+ # self, ticker: str, trades: list, equity_curve: list, metrics: dict, period: str
1104
+ # ) -> str:
1105
+ # """Format backtest results for Telegram"""
1106
+ # try:
1107
+ # if not trades or len(trades) == 0:
1108
+ # return (
1109
+ # f"📊 <b>Backtest Results: {ticker} ({period})</b>\n\n"
1110
+ # f"⚠️ No trades generated\n"
1111
+ # f"The MACD strategy did not generate any signals on this timeframe."
1112
+ # )
1113
+ #
1114
+ # # Extract metrics
1115
+ # win_rate = metrics.get('win_rate', 0)
1116
+ # total_return = metrics.get('total_return', 0)
1117
+ # sharpe = metrics.get('sharpe_ratio', 0)
1118
+ # max_dd = metrics.get('max_drawdown', 0)
1119
+ # profit_factor = metrics.get('profit_factor', 0)
1120
+ # trades_count = metrics.get('total_trades', 0)
1121
+ #
1122
+ # # Emojis based on metrics
1123
+ # return_emoji = "📈" if total_return > 0 else "📉"
1124
+ # wr_emoji = "🟢" if win_rate > 0.5 else "🔴" if win_rate < 0.3 else "🟡"
1125
+ # sharpe_emoji = "🟢" if sharpe > 1.0 else "🟡" if sharpe > 0 else "🔴"
1126
+ #
1127
+ # result = f"""
1128
+ # 📊 <b>Backtest Results: {ticker}</b>
1129
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1130
+ # ⏰ Period: {period}
1131
+ # 📉 Data Points: {len(equity_curve)} candles
1132
+ #
1133
+ # 📈 <b>Performance Metrics:</b>
1134
+ # {return_emoji} Total Return: {total_return:+.2f}%
1135
+ # 🎯 Win Rate: {win_rate*100:.1f}% {wr_emoji}
1136
+ # 📊 Profit Factor: {profit_factor:.2f}
1137
+ # 💹 Sharpe Ratio: {sharpe:.2f} {sharpe_emoji}
1138
+ # 📉 Max Drawdown: {max_dd*100:.1f}%
1139
+ #
1140
+ # 📋 <b>Trade Statistics:</b>
1141
+ # 🔔 Total Trades: {trades_count}
1142
+ # ✅ Winning Trades: {sum(1 for t in trades if t.get('pnl', 0) > 0)}
1143
+ # Losing Trades: {sum(1 for t in trades if t.get('pnl', 0) < 0)}
1144
+ #
1145
+ # 💰 <b>Trade Summary (first 5):</b>
1146
+ # """
1147
+ # for i, trade in enumerate(trades[:5]):
1148
+ # side = "BUY" if trade.get('side') == 'buy' else "SELL"
1149
+ # entry = trade.get('entry_price', 0)
1150
+ # exit_p = trade.get('exit_price', 0)
1151
+ # pnl = trade.get('pnl', 0)
1152
+ # emoji = "✅" if pnl > 0 else "❌"
1153
+ # result += f"{emoji} {i+1}. {side} @ ${entry:.2f} → ${exit_p:.2f} | P&L: ${pnl:+.2f}\n"
1154
+ #
1155
+ # result += f"\n⚠️ <b>Disclaimer:</b>\n"
1156
+ # result += f"Backtesting results are based on historical data.\n"
1157
+ # result += f"Past performance does not guarantee future results.\n"
1158
+ # result += f"Use as analysis tool only, not trading advice."
1159
+ #
1160
+ # return result
1161
+ #
1162
+ # except Exception as e:
1163
+ # logger.error(f"Error formatting backtest results: {e}")
1164
+ # return f"❌ Error formatting backtest results: {str(e)}"
1165
+
1166
+ # async def process_approval_callback(self, approval_id: str, action: str, chat_id: int) -> None:
1167
+ # """
1168
+ # Process approval callback from Telegram buttons
1169
+ # Called when user clicks approve/reject button
1170
+ #
1171
+ # Args:
1172
+ # approval_id: ID of the approval to process
1173
+ # action: 'approve' or 'reject'
1174
+ # chat_id: Chat ID for sending confirmation
1175
+ # """
1176
+ # try:
1177
+ # if not self.live_trader:
1178
+ # await self.send_message_via_proxy(
1179
+ # chat_id,
1180
+ # "❌ Live trader not initialized"
1181
+ # )
1182
+ # return
1183
+ #
1184
+ # if action.lower() == "approve":
1185
+ # result = await self.live_trader.approve_signal(approval_id)
1186
+ # if result:
1187
+ # await self.send_message_via_proxy(
1188
+ # chat_id,
1189
+ # f" <b>Signal Approved</b>\n\n"
1190
+ # f"Approval ID: {approval_id}\n"
1191
+ # f"Status: Executing order..."
1192
+ # )
1193
+ # logger.info(f"Signal {approval_id} approved by user")
1194
+ # else:
1195
+ # await self.send_message_via_proxy(
1196
+ # chat_id,
1197
+ # f"❌ Approval {approval_id} not found or already processed"
1198
+ # )
1199
+ #
1200
+ # elif action.lower() == "reject":
1201
+ # result = await self.live_trader.reject_signal(approval_id)
1202
+ # if result:
1203
+ # await self.send_message_via_proxy(
1204
+ # chat_id,
1205
+ # f" <b>Signal Rejected</b>\n\n"
1206
+ # f"Approval ID: {approval_id}\n"
1207
+ # f"Status: Signal skipped"
1208
+ # )
1209
+ # logger.info(f"Signal {approval_id} rejected by user")
1210
+ # else:
1211
+ # await self.send_message_via_proxy(
1212
+ # chat_id,
1213
+ # f"❌ Approval {approval_id} not found or already processed"
1214
+ # )
1215
+ #
1216
+ # except Exception as e:
1217
+ # logger.error(f"Error processing approval callback: {e}", exc_info=True)
1218
+ # await self.send_message_via_proxy(
1219
+ # chat_id,
1220
+ # f"❌ Error processing approval: {str(e)}"
1221
+ # )