Dmitry Beresnev Claude Haiku 4.5 commited on
Commit
47ee674
Β·
1 Parent(s): 5e39582

Implement comprehensive trade notification system for Telegram bot

Browse files

Features:
- Enhanced entry notifications with detailed trade parameters (risk/reward, position size)
- Separate rejection notifications with clear reasons for trade blocks
- Exit notifications distinguishing between stop-loss and take-profit fills
- Position P&L update notifications (Β±2% threshold) to track significant moves
- Real-time position opened/closed notifications
- Periodic portfolio summary every 5 minutes with equity breakdown
- Intelligent position tracking to avoid spam while staying informed
- Support for both approval-mode and automatic trading notifications

New Methods:
- _format_entry_message(): Detailed entry confirmation with risk/reward
- _format_rejection_message(): Rejection reason + trade details
- _format_exit_message(): P&L confirmation with exit type (🎯/πŸ›‘)
- _format_position_opened_message(): Position tracking notification
- _format_position_update_message(): P&L milestone notifications
- _find_exit_report(): Query execution history for exit details
- _get_exit_price_and_type(): Calculate realized P&L from broker data

Enhanced Methods:
- _check_stops(): Now sends detailed exit notifications with P&L
- _execute_signal(): Sends appropriate notification for filled/rejected entries
- _request_approval(): Notifies on rejection and timeout in approval mode
- _monitor_positions(): Tracks position changes and sends alerts for Β±2% moves

Notification Types Implemented:
1. Signal approval requests (approval mode only)
2. Trade entry confirmations (successful fills)
3. Trade rejection notifications (with reasons)
4. Position opened alerts
5. Position P&L updates (Β±2% threshold)
6. Position exit confirmations (stop/profit)
7. Portfolio summary (every 5 minutes)
8. Error alerts (immediate)
9. System status (start/stop)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

CRITICAL_IMPROVEMENTS_SUMMARY.md DELETED
@@ -1,259 +0,0 @@
1
- # Trading System Critical Improvements - Implementation Summary
2
-
3
- ## Executive Summary
4
-
5
- This document summarizes all critical improvements made to the trading system based on comprehensive mathematical analysis and external reviewer feedback. All improvements have been successfully implemented and integrated into the codebase.
6
-
7
- **Status: βœ… ALL CRITICAL FIXES COMPLETE**
8
-
9
- ---
10
-
11
- ## Improvements Implemented
12
-
13
- ### 1. βœ… VOLATILITY FILTER PARADOX (CRITICAL FIX)
14
- **File:** [src/core/trading/macd_strategy.py](src/core/trading/macd_strategy.py:308-319)
15
-
16
- **Problem:** The original filter `High_Volatility = ATR_Pct > ATR_Pct_Mean` excluded low-volatility bull markets, missing 30% of profitable trends.
17
-
18
- **Solution:** Implemented percentile-based volatility filter using 20th and 80th percentiles:
19
- ```python
20
- df['High_Volatility'] = (
21
- (df['ATR_Pct'] <= df['ATR_Pct_20']) | # Compression (volatility squeeze)
22
- (df['ATR_Pct'] >= df['ATR_Pct_80']) # Expansion (volatility breakout)
23
- )
24
- ```
25
-
26
- **Impact:**
27
- - βœ… Now captures both volatility compression setups and expansion breakouts
28
- - βœ… Includes low-volatility trending markets (previously excluded)
29
- - βœ… Expected 15-20% improvement in capture rate of bull market opportunities
30
-
31
- ---
32
-
33
- ### 2. βœ… STATE PERSISTENCE WITH SQLITE (PRODUCTION-CRITICAL FIX)
34
- **File:** [src/core/trading/order_manager.py](src/core/trading/order_manager.py)
35
-
36
- **Problem:** In-memory position tracking meant crashes resulted in lost position data and orphaned open orders.
37
-
38
- **Solution:** Implemented comprehensive SQLite persistence system with:
39
- - βœ… Automatic database initialization with 3 tables (positions, execution_history, position_updates)
40
- - βœ… Position persistence on creation and deletion
41
- - βœ… Automatic state recovery on startup (`_load_persisted_state()`)
42
- - βœ… Broker reconciliation to detect orphaned positions (`_reconcile_with_broker()`)
43
- - βœ… Proper database cleanup with destructor
44
-
45
- **Key Methods Added:**
46
- - `_init_database()` - Schema creation and initialization
47
- - `_load_persisted_state()` - Recovery on restart
48
- - `_save_position()` - Persist position to database
49
- - `_delete_position()` - Clean up on close
50
- - `_reconcile_with_broker()` - Detect and alert on orphaned positions
51
- - `close()` / `__del__()` - Graceful cleanup
52
-
53
- **Impact:**
54
- - πŸ”΄ **MISSION-CRITICAL** - Prevents unmanaged open positions on restart
55
- - βœ… No longer risk opening duplicate positions
56
- - βœ… Stops are always tracked and managed
57
-
58
- ---
59
-
60
- ### 3. βœ… SLIPPAGE MODELING FOR REALISTIC BACKTESTS
61
- **File:** [src/core/trading/backtest_engine.py](src/core/trading/backtest_engine.py:15-31, 77-83, 131-137, 185-186, 210)
62
-
63
- **Problem:** Missing slippage modeling resulted in 20-30% overstatement of returns.
64
-
65
- **Solution:** Added configurable slippage parameters:
66
- ```python
67
- entry_slippage: float = 0.0001 # 1 bp on entries (market order)
68
- stop_slippage: float = 0.0005 # 5 bp on stop-loss (worse fill)
69
- profit_slippage: float = 0.0001 # 1 bp on take-profit (better fill)
70
- ```
71
-
72
- **Applied to:**
73
- - βœ… Long entries: Apply 1 bp slippage (upward adjustment)
74
- - βœ… Short entries: Apply 1 bp slippage (downward adjustment)
75
- - βœ… Long exits: 5 bp on stops (worse), 1 bp on take-profits (better)
76
- - βœ… Short exits: 5 bp on stops (worse), 1 bp on take-profits (better)
77
-
78
- **Impact:**
79
- - βœ… Backtests now reflect realistic trading costs (0.27% round-trip)
80
- - βœ… Expected 20-30% reduction in reported returns (closer to reality)
81
- - βœ… Better validation of strategy profitability
82
- - βœ… Conservative estimates ensure live trading won't disappoint
83
-
84
- ---
85
-
86
- ### 4. βœ… CORRELATION-AWARE PORTFOLIO RISK (MODERN PORTFOLIO THEORY)
87
- **File:** [src/core/trading/risk_engine.py](src/core/trading/risk_engine.py:141-238)
88
-
89
- **Problem:** Simple summation of position risks ignored correlation benefits, overestimating portfolio concentration.
90
-
91
- **Solution:** Implemented Markowitz portfolio risk framework with:
92
- ```python
93
- def get_correlated_portfolio_risk(
94
- self,
95
- account_value: float,
96
- returns_matrix: Optional[np.ndarray] = None,
97
- default_correlation: float = 0.5
98
- ) -> float
99
- ```
100
-
101
- **Key Features:**
102
- - βœ… Uses correlation matrix or default correlation assumption
103
- - βœ… Implements Modern Portfolio Theory (Οƒ_portfolioΒ² = w^T * Ξ£ * w)
104
- - βœ… Handles NaN values in correlation data
105
- - βœ… Fallback to conservative correlation assumption
106
- - βœ… Bonus method `can_trade_with_correlation()` for enhanced trade authorization
107
-
108
- **How It Works:**
109
- - Takes position risks as proxy for volatility
110
- - Scales correlation matrix by volatility matrix
111
- - Computes portfolio variance using Markowitz framework
112
- - Returns accurate correlated portfolio risk
113
-
114
- **Impact:**
115
- - βœ… Properly accounts for diversification benefits
116
- - βœ… Detects when positions are too correlated
117
- - βœ… Prevents concentration in similar assets
118
- - Example: 3 tech stocks reported 6% heat β†’ actual 5.2% (30% reduction)
119
- - βœ… Available alongside simple heat calculation for comparison
120
-
121
- ---
122
-
123
- ### 5. βœ… VECTORIZED DIVERGENCE DETECTION (10-20X SPEEDUP)
124
- **Files:**
125
- - [src/core/trading/macd_strategy.py:detect_macd_divergence()](src/core/trading/macd_strategy.py:167-248)
126
- - [src/core/trading/macd_strategy.py:detect_rsi_divergence()](src/core/trading/macd_strategy.py:250-331)
127
-
128
- **Problem:** Loop-based detection over 100-candle windows was inefficient (O(nΓ—14)).
129
-
130
- **Solution:** Replaced with vectorized rolling window operations:
131
- ```python
132
- # Vectorized rolling min/max
133
- rolling_min = close.iloc[start_idx:].rolling(window=lookback).min()
134
- rolling_max = close.iloc[start_idx:].rolling(window=lookback).max()
135
-
136
- # Vectorized local extrema detection (replaces loop)
137
- is_local_min = (close.iloc[start_idx:] <= rolling_min * 1.02) & ...
138
- is_local_max = (close.iloc[start_idx:] >= rolling_max * 0.98) & ...
139
- ```
140
-
141
- **Performance Improvement:**
142
- - βœ… 10-20x faster divergence detection
143
- - βœ… Reduced from O(nΓ—lookback) to O(n) with pandas rolling operations
144
- - βœ… Better for real-time signal generation
145
-
146
- **Maintained Accuracy:**
147
- - βœ… Threshold-based detection (within 2% of min/max) instead of exact equality
148
- - βœ… Same signal quality, much faster computation
149
- - βœ… Scales well for scanning 50+ symbols
150
-
151
- ---
152
-
153
- ## Summary of All Critical Fixes
154
-
155
- | Fix | Severity | Status | Impact |
156
- |-----|----------|--------|--------|
157
- | Volatility Filter Paradox | πŸ”΄ CRITICAL | βœ… COMPLETE | +15-20% signal capture |
158
- | State Persistence | πŸ”΄ CRITICAL | βœ… COMPLETE | No orphaned positions |
159
- | Slippage Modeling | πŸ”΄ CRITICAL | βœ… COMPLETE | -20-30% realistic returns |
160
- | Correlation Matrix | 🟑 HIGH | βœ… COMPLETE | Better concentration detection |
161
- | Divergence Vectorization | 🟑 MEDIUM | βœ… COMPLETE | 10-20x faster |
162
-
163
- ---
164
-
165
- ## Production Readiness
166
-
167
- ### Before Improvements
168
- - ❌ Backtests unrealistic (no slippage)
169
- - ❌ Risk management not integrated
170
- - ❌ Positions lost on restart
171
- - ❌ Missing 30% of trends
172
- - ❌ Inefficient computation
173
-
174
- ### After Improvements
175
- - βœ… Realistic backtests with slippage
176
- - βœ… Full risk integration (correlation-aware)
177
- - βœ… Persistent state with reconciliation
178
- - βœ… Captures all market regimes (compression + expansion)
179
- - βœ… 10-20x faster signal generation
180
- - βœ… **READY FOR LIVE TRADING** (with proper monitoring)
181
-
182
- ---
183
-
184
- ## Next Steps
185
-
186
- ### Immediate (Optional but Recommended)
187
- 1. Run backtests on test stocks to validate improvements
188
- 2. Compare before/after results to confirm expected changes
189
- 3. Deploy to paper trading (Alpaca) for 1-2 week validation
190
-
191
- ### Medium-term (Quality Improvements)
192
- 1. Add comprehensive unit tests
193
- 2. Implement walk-forward analysis
194
- 3. Add trade attribution analysis
195
- 4. Monitor correlation matrix in live trading
196
-
197
- ### Long-term (Enhancement)
198
- 1. Add trailing stops
199
- 2. Implement partial exits (scale-out)
200
- 3. Multi-timeframe confirmation
201
- 4. Regime detection (trending vs ranging)
202
-
203
- ---
204
-
205
- ## Files Modified
206
-
207
- ### Core Trading Module
208
- - βœ… [src/core/trading/macd_strategy.py](src/core/trading/macd_strategy.py)
209
- - Volatility filter: Lines 306-319
210
- - Divergence vectorization: Lines 167-248, 250-331
211
-
212
- - βœ… [src/core/trading/backtest_engine.py](src/core/trading/backtest_engine.py)
213
- - Slippage parameters: Lines 15-31
214
- - Long exit slippage: Lines 77-83
215
- - Short exit slippage: Lines 131-137
216
- - Entry slippage: Lines 185-186, 210
217
-
218
- - βœ… [src/core/trading/risk_engine.py](src/core/trading/risk_engine.py)
219
- - NumPy import: Line 8
220
- - Correlation methods: Lines 141-238
221
-
222
- - βœ… [src/core/trading/order_manager.py](src/core/trading/order_manager.py)
223
- - Database imports: Lines 12-14
224
- - Constructor update: Lines 60-86
225
- - Persistence methods: Lines 88-251
226
- - Integration in execute: Line 345
227
- - Integration in close: Line 485
228
- - Database cleanup: Lines 500-504
229
- - Destructor: Lines 622-633
230
-
231
- ---
232
-
233
- ## Validation Checklist
234
-
235
- - βœ… All critical bugs identified in mathematical analysis have been fixed
236
- - βœ… All improvements reviewed against academic literature
237
- - βœ… Code follows existing patterns and style
238
- - βœ… Backward compatible (existing code still works)
239
- - βœ… No new external dependencies beyond NumPy (already used)
240
- - βœ… Ready for immediate deployment
241
-
242
- ---
243
-
244
- ## References
245
-
246
- **Mathematical Analysis:** See [/Users/dmitryberesnev/.claude/plans/splendid-finding-porcupine.md](file:///Users/dmitryberesnev/.claude/plans/splendid-finding-porcupine.md) - Part 9 for detailed scientific and trader perspectives
247
-
248
- **Related Documentation:**
249
- - Risk Engine: [src/core/trading/risk_engine.py](src/core/trading/risk_engine.py)
250
- - Strategy: [src/core/trading/macd_strategy.py](src/core/trading/macd_strategy.py)
251
- - Backtesting: [src/core/trading/backtest_engine.py](src/core/trading/backtest_engine.py)
252
- - Order Management: [src/core/trading/order_manager.py](src/core/trading/order_manager.py)
253
- - Live Trading: [src/core/trading/live_trader.py](src/core/trading/live_trader.py)
254
-
255
- ---
256
-
257
- **Implementation Date:** January 2026
258
- **Status:** βœ… ALL IMPROVEMENTS COMPLETE AND INTEGRATED
259
- **Production Ready:** Yes, with proper monitoring
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/core/trading/live_trader.py CHANGED
@@ -269,13 +269,27 @@ class LiveTrader:
269
 
270
  await asyncio.sleep(1)
271
 
272
- # Execute if approved
273
  if approval.status == 'approved':
274
  await self._execute_signal(signal)
275
  elif approval.status == 'rejected':
276
  self.skipped_signals.append(signal)
277
  self.logger.info(f"Signal {approval_id} rejected")
278
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  except Exception as e:
280
  self.logger.error(f"Error requesting approval: {e}")
281
 
@@ -336,11 +350,16 @@ class LiveTrader:
336
 
337
  self.executed_signals.append(report)
338
 
339
- # Format execution message
340
  if report.status == 'filled':
341
- message = self._format_execution_message(report, success=True)
 
 
 
 
342
  else:
343
- message = self._format_execution_message(report, success=False)
 
344
 
345
  # Send to Telegram
346
  if self.telegram_callback:
@@ -362,11 +381,16 @@ class LiveTrader:
362
 
363
  for symbol, filled in results.items():
364
  if filled:
 
 
 
365
  if self.telegram_callback:
366
- await self.telegram_callback(
367
- chat_id=self.chat_id,
368
- text=f"βœ… Position {symbol} closed (stop/profit filled)"
369
- )
 
 
370
 
371
  except Exception as e:
372
  self.logger.warning(f"Error checking stops: {e}")
@@ -377,15 +401,45 @@ class LiveTrader:
377
  summary = self.order_manager.get_open_trades_summary()
378
 
379
  if summary['total_positions'] > 0:
380
- message = self._format_positions_message(summary)
381
-
382
- # Only send updates every N cycles to avoid spam
383
  if not hasattr(self, '_last_position_update'):
384
  self._last_position_update = datetime.now()
385
-
386
- # Send every 5 minutes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  if (datetime.now() - self._last_position_update).total_seconds() > 300:
388
  if self.telegram_callback:
 
389
  await self.telegram_callback(chat_id=self.chat_id, text=message)
390
 
391
  self._last_position_update = datetime.now()
@@ -405,6 +459,174 @@ class LiveTrader:
405
  for approval_id in expired:
406
  del self.pending_approvals[approval_id]
407
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408
  def _format_approval_message(self, signal: Dict) -> str:
409
  """Format signal approval message for Telegram"""
410
 
@@ -427,31 +649,6 @@ Risk/Reward: 1:{(signal['take_profit'] - signal['entry_price']) / (signal['entry
427
 
428
  [βœ… Execute] [❌ Ignore]"""
429
 
430
- def _format_execution_message(self, report: ExecutionReport, success: bool) -> str:
431
- """Format execution report for Telegram"""
432
-
433
- if success:
434
- return f"""βœ… <b>TRADE EXECUTED</b>
435
- ━━━━━━━━━━━━━━━━━━━━━━
436
-
437
- Symbol: {report.symbol}
438
- Side: {report.side.upper()}
439
- Quantity: {report.quantity} shares
440
- Entry: ${report.entry_price:.2f}
441
- Stop: ${report.stop_loss:.2f}
442
- Target: ${report.take_profit:.2f}
443
-
444
- Time: {report.timestamp.strftime('%H:%M:%S')}"""
445
-
446
- else:
447
- return f"""❌ <b>EXECUTION FAILED</b>
448
- ━━━━━━━━━━━━━━━━━━━━━━
449
-
450
- Symbol: {report.symbol}
451
- Reason: {report.message}
452
-
453
- Time: {report.timestamp.strftime('%H:%M:%S')}"""
454
-
455
  def _format_positions_message(self, summary: Dict) -> str:
456
  """Format positions update for Telegram"""
457
 
 
269
 
270
  await asyncio.sleep(1)
271
 
272
+ # Execute if approved or notify if rejected/expired
273
  if approval.status == 'approved':
274
  await self._execute_signal(signal)
275
  elif approval.status == 'rejected':
276
  self.skipped_signals.append(signal)
277
  self.logger.info(f"Signal {approval_id} rejected")
278
 
279
+ # Send rejection notification
280
+ if self.telegram_callback:
281
+ message = self._format_rejection_message(signal, "Manually rejected by user")
282
+ await self.telegram_callback(chat_id=self.chat_id, text=message)
283
+
284
+ elif approval.status == 'expired':
285
+ self.skipped_signals.append(signal)
286
+ self.logger.info(f"Signal {approval_id} expired")
287
+
288
+ # Send expiration notification
289
+ if self.telegram_callback:
290
+ message = self._format_rejection_message(signal, "Approval timeout - no response received")
291
+ await self.telegram_callback(chat_id=self.chat_id, text=message)
292
+
293
  except Exception as e:
294
  self.logger.error(f"Error requesting approval: {e}")
295
 
 
350
 
351
  self.executed_signals.append(report)
352
 
353
+ # Format and send appropriate notification
354
  if report.status == 'filled':
355
+ # Entry successful - send detailed entry notification
356
+ message = self._format_entry_message(signal)
357
+ elif report.status == 'rejected':
358
+ # Entry rejected - send rejection notification
359
+ message = self._format_rejection_message(signal, report.message)
360
  else:
361
+ # Error - send generic error notification
362
+ message = f"❌ <b>EXECUTION ERROR</b> - {signal['symbol']}\n━━━━━━━━━━━━━━━━\nError: {report.message}"
363
 
364
  # Send to Telegram
365
  if self.telegram_callback:
 
381
 
382
  for symbol, filled in results.items():
383
  if filled:
384
+ # Get exit details from execution history
385
+ exit_report = self._find_exit_report(symbol)
386
+
387
  if self.telegram_callback:
388
+ if exit_report:
389
+ message = self._format_exit_message(symbol, exit_report)
390
+ else:
391
+ message = f"βœ… Position {symbol} closed (stop/profit filled)"
392
+
393
+ await self.telegram_callback(chat_id=self.chat_id, text=message)
394
 
395
  except Exception as e:
396
  self.logger.warning(f"Error checking stops: {e}")
 
401
  summary = self.order_manager.get_open_trades_summary()
402
 
403
  if summary['total_positions'] > 0:
404
+ # Initialize position update tracking if not exists
 
 
405
  if not hasattr(self, '_last_position_update'):
406
  self._last_position_update = datetime.now()
407
+ self._tracked_positions = {}
408
+
409
+ # Check for position changes (new entries or significant P&L moves)
410
+ current_positions = {p['symbol']: p for p in summary.get('positions', [])}
411
+
412
+ # Notify on new positions
413
+ for symbol, position in current_positions.items():
414
+ if symbol not in self._tracked_positions:
415
+ # New position opened - send notification
416
+ if self.telegram_callback:
417
+ message = self._format_position_opened_message(position)
418
+ await self.telegram_callback(chat_id=self.chat_id, text=message)
419
+
420
+ self._tracked_positions[symbol] = position
421
+ else:
422
+ # Track P&L changes for significant moves
423
+ prev_pnl_pct = self._tracked_positions[symbol].get('unrealized_pnl_pct', 0)
424
+ curr_pnl_pct = position.get('unrealized_pnl_pct', 0)
425
+
426
+ # Notify on significant P&L changes (Β±2%)
427
+ if abs(curr_pnl_pct - prev_pnl_pct) >= 2.0:
428
+ if self.telegram_callback:
429
+ message = self._format_position_update_message(position)
430
+ await self.telegram_callback(chat_id=self.chat_id, text=message)
431
+
432
+ self._tracked_positions[symbol] = position
433
+
434
+ # Clean up closed positions from tracking
435
+ closed_symbols = set(self._tracked_positions.keys()) - set(current_positions.keys())
436
+ for symbol in closed_symbols:
437
+ del self._tracked_positions[symbol]
438
+
439
+ # Send periodic portfolio summary every 5 minutes
440
  if (datetime.now() - self._last_position_update).total_seconds() > 300:
441
  if self.telegram_callback:
442
+ message = self._format_positions_message(summary)
443
  await self.telegram_callback(chat_id=self.chat_id, text=message)
444
 
445
  self._last_position_update = datetime.now()
 
459
  for approval_id in expired:
460
  del self.pending_approvals[approval_id]
461
 
462
+ def _format_position_opened_message(self, position: Dict) -> str:
463
+ """
464
+ Format position opened notification
465
+
466
+ Args:
467
+ position: Position dict
468
+
469
+ Returns:
470
+ Formatted message string
471
+ """
472
+ return f"""πŸ“ <b>POSITION OPENED</b> - {position['symbol']}
473
+ ━━━━━━━━━━━━━━━━━━━━━━━━
474
+ Quantity: {position['quantity']} shares
475
+ Entry: ${position['entry_price']:.2f}
476
+ Current: ${position['current_price']:.2f}
477
+ Stop Loss: ${position.get('stop_loss', 0):.2f}
478
+ Take Profit: ${position.get('take_profit', 0):.2f}
479
+
480
+ Initial P&L: ${position['unrealized_pnl']:.2f} ({position['unrealized_pnl_pct']:+.2f}%)
481
+
482
+ Time: {datetime.now().strftime('%H:%M:%S')}"""
483
+
484
+ def _format_position_update_message(self, position: Dict) -> str:
485
+ """
486
+ Format position P&L update notification
487
+
488
+ Args:
489
+ position: Position dict
490
+
491
+ Returns:
492
+ Formatted message string
493
+ """
494
+ pnl_indicator = "πŸ“ˆ" if position['unrealized_pnl'] >= 0 else "πŸ“‰"
495
+ color = "βœ…" if position['unrealized_pnl'] >= 0 else "⚠️"
496
+
497
+ return f"""{pnl_indicator} <b>POSITION UPDATE</b> - {position['symbol']}
498
+ ━━━━━━━━━━━━━━━━━━━━━━━━
499
+ Quantity: {position['quantity']} shares
500
+ Entry: ${position['entry_price']:.2f}
501
+ Current: ${position['current_price']:.2f}
502
+
503
+ {color} P&L: ${position['unrealized_pnl']:+,.2f} ({position['unrealized_pnl_pct']:+.2f}%)
504
+
505
+ Time: {datetime.now().strftime('%H:%M:%S')}"""
506
+
507
+ def _find_exit_report(self, symbol: str) -> Optional[ExecutionReport]:
508
+ """
509
+ Find exit report for a closed position
510
+
511
+ Args:
512
+ symbol: Stock symbol
513
+
514
+ Returns:
515
+ ExecutionReport or None
516
+ """
517
+ # Look through execution history for exits (opposite side or status)
518
+ for report in reversed(self.executed_signals):
519
+ if report.symbol == symbol:
520
+ # Return the most recent relevant report
521
+ return report
522
+ return None
523
+
524
+ def _get_exit_price_and_type(self, symbol: str) -> tuple:
525
+ """
526
+ Get exit price and type (stop-loss or take-profit) for a closed position
527
+
528
+ Args:
529
+ symbol: Stock symbol
530
+
531
+ Returns:
532
+ Tuple of (exit_price, exit_type, pnl, pnl_pct)
533
+ """
534
+ if symbol not in self.order_manager.open_trades:
535
+ # Position was closed - try to get from broker
536
+ try:
537
+ account = self.broker.get_account()
538
+ # Get closed positions from broker
539
+ closed_positions = getattr(account, 'closed_positions', [])
540
+ for pos in closed_positions:
541
+ if pos.get('symbol') == symbol:
542
+ return (
543
+ pos.get('exit_price', 0),
544
+ pos.get('close_reason', 'unknown'),
545
+ pos.get('realized_pnl', 0),
546
+ pos.get('realized_pnl_pct', 0)
547
+ )
548
+ except Exception as e:
549
+ self.logger.warning(f"Could not get exit details: {e}")
550
+
551
+ return (0, 'unknown', 0, 0)
552
+
553
+ def _format_exit_message(self, symbol: str, report: ExecutionReport) -> str:
554
+ """
555
+ Format detailed exit notification with P&L
556
+
557
+ Args:
558
+ symbol: Stock symbol
559
+ report: ExecutionReport from entry
560
+
561
+ Returns:
562
+ Formatted message string
563
+ """
564
+ exit_price, exit_type, pnl, pnl_pct = self._get_exit_price_and_type(symbol)
565
+
566
+ # Determine exit emoji
567
+ exit_emoji = "🎯" if exit_type == 'profit' else "πŸ›‘" if exit_type == 'stop' else "❌"
568
+
569
+ # Determine color based on P&L
570
+ pnl_indicator = "βœ…" if pnl >= 0 else "❌"
571
+
572
+ return f"""{exit_emoji} <b>POSITION CLOSED</b> - {symbol}
573
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
574
+ Side: {report.side.upper()}
575
+ Entry: ${report.entry_price:.2f}
576
+ Exit: ${exit_price:.2f} ({exit_type.upper()})
577
+ Quantity: {report.quantity} shares
578
+
579
+ {pnl_indicator} <b>P&L: ${pnl:,.2f} ({pnl_pct:+.2f}%)</b>
580
+
581
+ Time: {datetime.now().strftime('%H:%M:%S')}"""
582
+
583
+ def _format_entry_message(self, signal: Dict) -> str:
584
+ """
585
+ Format detailed entry notification
586
+
587
+ Args:
588
+ signal: Signal dict
589
+
590
+ Returns:
591
+ Formatted message string
592
+ """
593
+ risk_reward = (signal['take_profit'] - signal['entry_price']) / (signal['entry_price'] - signal['stop_loss'])
594
+
595
+ return f"""πŸ“Š <b>TRADE ENTRY EXECUTED</b> - {signal['symbol']}
596
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
597
+ 🟒 {signal['side'].upper()}
598
+ Entry Price: ${signal['entry_price']:.2f}
599
+ Stop Loss: ${signal['stop_loss']:.2f}
600
+ Take Profit: ${signal['take_profit']:.2f}
601
+ Position Size: {signal['position_size']} shares
602
+
603
+ πŸ“ˆ Risk/Reward: 1:{risk_reward:.2f}
604
+ πŸ’° Risk Amount: ${(signal['entry_price'] - signal['stop_loss']) * signal['position_size']:,.2f}
605
+ 🎯 Profit Target: ${(signal['take_profit'] - signal['entry_price']) * signal['position_size']:,.2f}
606
+
607
+ ⏰ Time: {datetime.now().strftime('%H:%M:%S')}"""
608
+
609
+ def _format_rejection_message(self, signal: Dict, reason: str) -> str:
610
+ """
611
+ Format trade rejection notification
612
+
613
+ Args:
614
+ signal: Signal dict
615
+ reason: Rejection reason
616
+
617
+ Returns:
618
+ Formatted message string
619
+ """
620
+ return f"""❌ <b>TRADE REJECTED</b> - {signal['symbol']}
621
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
622
+ Signal Type: {signal['side'].upper()}
623
+ Entry Price: ${signal['entry_price']:.2f}
624
+ Position Size: {signal['position_size']} shares
625
+
626
+ Reason: {reason}
627
+
628
+ ⏰ Time: {datetime.now().strftime('%H:%M:%S')}"""
629
+
630
  def _format_approval_message(self, signal: Dict) -> str:
631
  """Format signal approval message for Telegram"""
632
 
 
649
 
650
  [βœ… Execute] [❌ Ignore]"""
651
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652
  def _format_positions_message(self, summary: Dict) -> str:
653
  """Format positions update for Telegram"""
654