-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbot.py
More file actions
2545 lines (2190 loc) · 109 KB
/
Copy pathbot.py
File metadata and controls
2545 lines (2190 loc) · 109 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""Bot de Telegram para gestión del portfolio de trading.
Mejoras: HTML formatting, gráficas, heatmap de mercado, gestión de watchlist,
comparativas, y visualización completa.
"""
import io
import logging
import re
from datetime import datetime
from telegram import BotCommand, Update
from telegram.constants import ParseMode
from telegram.ext import (
Application, CommandHandler, MessageHandler, ContextTypes, filters
)
import database as db
from analyzer import get_best_signal, quick_analysis, analyze_ticker
from portfolio import (
execute_buy, execute_sell, get_portfolio_summary,
check_stop_loss_take_profit, get_current_price
)
from tracker import tracker
from config import TELEGRAM_TOKEN, TELEGRAM_CHAT_ID
from traderepublic import tr_sync
logger = logging.getLogger(__name__)
# Mapeo de nombres a tickers
COMPANY_NAMES = {
# Tech
"apple": "AAPL", "microsoft": "MSFT", "google": "GOOGL",
"alphabet": "GOOGL", "amazon": "AMZN", "nvidia": "NVDA",
"meta": "META", "facebook": "META", "tesla": "TSLA",
"amd": "AMD", "netflix": "NFLX", "salesforce": "CRM",
"broadcom": "AVGO", "jpmorgan": "JPM", "visa": "V",
"intel": "INTC", "ibm": "IBM", "oracle": "ORCL",
"palantir": "PLTR", "uber": "UBER", "airbnb": "ABNB",
"paypal": "PYPL", "shopify": "SHOP", "spotify": "SPOT",
"disney": "DIS", "coca cola": "KO", "cocacola": "KO",
"pepsi": "PEP", "walmart": "WMT", "boeing": "BA",
"mastercard": "MA", "lockheed": "LMT", "lockheed martin": "LMT",
"lockeed": "LMT", "lockeed martin": "LMT",
"raytheon": "RTX", "northrop": "NOC", "northrop grumman": "NOC",
"general dynamics": "GD", "general electric": "GE",
"johnson": "JNJ", "johnson & johnson": "JNJ",
"procter": "PG", "procter gamble": "PG",
"exxon": "XOM", "exxonmobil": "XOM", "chevron": "CVX",
"berkshire": "BRK-B", "warren buffett": "BRK-B",
"costco": "COST", "starbucks": "SBUX", "nike": "NKE",
"adobe": "ADBE", "qualcomm": "QCOM", "cisco": "CSCO",
"repsol": "REP.MC", "inditex": "ITX.MC", "santander": "SAN.MC",
"bbva": "BBVA.MC", "telefonica": "TEF.MC", "telefónica": "TEF.MC",
"iberdrola": "IBE.MC", "endesa": "ELE.MC", "caixabank": "CABK.MC",
"amadeus": "AMS.MC", "ferrovial": "FER.MC", "naturgy": "NTGY.MC",
"mapfre": "MAP.MC", "acciona": "ANA.MC", "aena": "AENA.MC",
# Commodities / Energía
"petroleo": "USO", "petróleo": "USO", "crudo": "USO",
"oil": "USO", "brent": "BNO", "wti": "USO",
"oro": "GLD", "gold": "GLD",
"plata": "SLV", "silver": "SLV",
"gas": "UNG", "gas natural": "UNG",
"cobre": "COPX", "copper": "COPX",
# ETFs / Índices
"spy": "SPY", "qqq": "QQQ", "nasdaq": "QQQ",
"sp500": "SPY", "s&p": "SPY", "s&p500": "SPY",
"dow": "DIA", "dow jones": "DIA",
"russell": "IWM", "emergentes": "EEM",
"europa": "VGK", "china": "FXI", "japon": "EWJ",
# Energía empresas
"shell": "SHEL", "bp": "BP", "total": "TTE",
# Bancos
"goldman": "GS",
"morgan stanley": "MS", "bank of america": "BAC",
# Crypto ETFs
"bitcoin": "IBIT", "btc": "IBIT", "ethereum": "ETHA", "eth": "ETHA",
}
# Palabras a ignorar en español
IGNORE_WORDS = {
"EL", "LA", "LOS", "LAS", "UN", "UNA", "DE", "DEL", "EN", "POR",
"PARA", "CON", "QUE", "COMO", "MAS", "MENOS", "HOLA", "GRACIAS",
"SI", "NO", "BUENO", "VALE", "OK", "BIEN", "MAL", "Y", "O",
"MI", "TU", "SU", "ES", "AL", "SE", "LE", "LO", "ME", "TE",
"HAY", "SOY", "VOY", "VER", "SER", "IR", "HAZ", "DI", "DA",
"TODO", "TODOS", "TODAS", "MUCHO", "POCO", "ALGO", "NADA",
"PUEDAS", "PUEDA", "PUEDE", "PUEDO", "MEJOR", "PEOR",
"TAL", "MUY", "YA", "AUN", "HOY", "QUE", "MIS", "SUS", "TUS",
"ADD", "DEL",
}
def extract_ticker(text: str) -> str | None:
"""Extrae un ticker del texto del usuario."""
text_lower = text.lower().strip()
for name, ticker in COMPANY_NAMES.items():
if name in text_lower:
return ticker
words = text.upper().split()
for word in words:
clean = re.sub(r'[^A-Z]', '', word)
if 1 <= len(clean) <= 5 and clean not in IGNORE_WORDS and clean.isalpha():
return clean
return None
def extract_multiple_tickers(text: str) -> list[str]:
"""Extrae múltiples tickers de un texto (para comparativas)."""
tickers = []
text_lower = text.lower().strip()
for name, ticker in COMPANY_NAMES.items():
if name in text_lower and ticker not in tickers:
tickers.append(ticker)
words = text.upper().split()
for word in words:
clean = re.sub(r'[^A-Z]', '', word)
if 2 <= len(clean) <= 5 and clean not in IGNORE_WORDS and clean.isalpha():
if clean not in tickers:
tickers.append(clean)
return tickers
def detect_intent(text: str) -> str:
"""Detecta la intención del mensaje."""
text_lower = text.lower().strip()
# Prioridad absoluta: prompt y comandos TR
if text_lower.startswith("prompt"):
return "prompt"
if text_lower.startswith("tr "):
if any(kw in text_lower for kw in ["conecta", "configura", "setup"]):
return "tr_setup"
if any(kw in text_lower for kw in ["confirma", "confirm", "código", "codigo"]):
return "tr_confirm"
if any(kw in text_lower for kw in ["portfolio", "cartera", "posiciones"]):
return "tr_portfolio"
intent_patterns = {
"greet": ["hola", "buenas", "hey", "buenos días", "buenas tardes"],
"thanks": ["gracias", "thx", "thanks", "genial", "perfecto"],
"help": ["ayuda", "help", "qué puedes", "comandos", "opciones"],
"demo": ["demo", "ejemplo", "tutorial", "enséñame", "enseñame"],
"guide": ["guía", "guia", "cómo funciona", "como funciona",
"qué es esto", "que es esto", "explícame", "explicame",
"cómo analiza", "como analiza", "algoritmo", "metodología",
"metodologia", "métricas", "metricas", "indicadores"],
"should_buy": ["compro?", "lo compro", "merece la pena", "es buen momento",
"debería comprar", "deberia comprar", "¿compro", "vale la pena",
"es buena idea", "qué opinas de", "que opinas de"],
"confirmed_buy": ["he comprado", "compré", "compre", "acabo de comprar",
"ya compré", "ya compre", "metí", "meti",
"he metido", "acabo de meter"],
"confirmed_sell": ["he vendido", "vendí", "vendi", "acabo de vender",
"ya vendí", "ya vendi", "he cerrado", "cerré", "cerre"],
"portfolio": ["portfolio", "cartera", "resumen", "estado", "cómo voy",
"mis posiciones", "mi dinero", "balance"],
"review_portfolio": ["revista", "revisa mi cartera", "revisa portfolio",
"cómo voy", "como voy", "qué hago", "que hago",
"analiza mi cartera", "analiza mi portfolio",
"qué vendo", "que vendo", "qué mantengo", "que mantengo"],
"optimize_portfolio": ["optimiza", "optimizar", "mejora mi cartera",
"mejora portfolio", "rota", "rotación", "rotacion",
"alternativas", "algo mejor", "cambios"],
"scan": ["escanea", "busca", "oportunidad", "señal", "scan",
"hay algo", "qué compro", "recomendación", "recomienda",
"analiza todo", "analiza todos", "todo lo que puedas"],
"analyze": ["analiza", "análisis", "qué tal", "cómo está",
"información de", "info de", "mira"],
"buy": ["compra", "comprar", "buy", "mete", "invierte"],
"sell": ["vende", "vender", "sell", "cierra", "sal de"],
"add_watch": ["añade", "añadir", "agrega", "agregar", "add"],
"remove_watch": ["quita", "quitar", "elimina", "eliminar", "remove", "borra"],
"watchlist": ["watchlist", "lista", "seguimiento", "vigilar"],
"tracker": ["tracker", "precisión", "rendimiento", "estadísticas",
"cómo van las señales", "aciertos"],
"history": ["historial", "operaciones", "trades", "cerradas"],
"news": ["noticias", "sentimiento", "news", "sentiment",
"qué dicen", "prensa", "headlines"],
"calendar": ["calendario", "eventos", "calendar", "fed",
"opec", "económico", "macro"],
"market": ["mercado", "market", "heatmap", "mapa", "overview",
"vista general", "resumen mercado"],
"compare": ["compara", "comparar", "compare", "vs", "versus"],
"chart": ["gráfica", "grafica", "chart", "gráfico", "grafico",
"dibuja", "pinta"],
"tr_sync": ["sincroniza", "sync", "trade republic", "tr sync",
"sincronizar"],
"momentum": ["momentum", "rotación", "rotacion", "etfs", "rotation",
"portfolio mensual", "mensual", "rebalanceo"],
"pead": ["pead", "earnings", "resultados empresa", "drift",
"post-earnings", "sorpresa earnings"],
"regime": ["régimen", "regimen", "hmm", "estado mercado",
"bull bear", "mercado hmm"],
"backtest": ["backtest", "backtesting", "prueba", "simula",
"simulación", "simulacion", "test estrategia"],
"optimize": ["optimiza", "optimizar", "optimize", "mejores parámetros",
"mejores parametros", "grid search"],
"ml_train": ["entrena", "entrenar", "train", "aprende", "aprender",
"entrenar modelo", "ml train"],
"ml_predict": ["predice", "predicción", "prediccion", "predict",
"ml", "modelo", "qué dice el modelo"],
"ml_approved": ["aprobados", "qué vigilas", "que vigilas", "qué miras",
"que miras", "tickers ml", "lista ml"],
"alert_add": ["avísame cuando", "avisame cuando", "ponme alerta",
"pon alerta", "vigila ", "crea alerta", "nueva alerta",
"alerta para"],
"alert_list": ["mis alertas", "alertas activas", "ver alertas",
"lista alertas"],
"alert_remove": ["quita alerta", "elimina alerta", "cancela alerta",
"borra alerta", "remove alert", "deja de vigilar"],
"invite": ["invitar", "invita", "invite", "código para", "codigo para",
"genera código", "genera codigo", "dar acceso", "dar de alta"],
"kick_user": ["eliminar usuario", "elimina usuario", "quita usuario",
"expulsar", "expulsa", "kick", "dar de baja", "baja usuario",
"bloquear", "bloquea"],
"users": ["usuarios", "users", "quién tiene acceso", "quien tiene acceso",
"invitados", "miembros"],
"tr_setup": ["tr conecta", "tr configura", "tr setup",
"conecta trade republic", "conectar tr"],
"tr_confirm": ["tr confirma", "tr confirm", "tr código", "tr codigo"],
"tr_portfolio": ["tr portfolio", "tr cartera", "tr posiciones",
"portfolio tr", "cartera tr"],
"prompt": ["prompt"],
}
# Primero: intents con keywords multi-palabra (más específicos)
priority_intents = ["alert_add", "alert_remove", "alert_list", "kick_user"]
for pi in priority_intents:
if pi in intent_patterns:
if any(kw in text_lower for kw in intent_patterns[pi]):
return pi
for intent, keywords in intent_patterns.items():
if intent in priority_intents:
continue
if any(kw in text_lower for kw in keywords):
return intent
return "unknown"
# ===== HTML Formatters =====
def h(text: str) -> str:
"""Escapa HTML."""
return text.replace("&", "&").replace("<", "<").replace(">", ">")
def format_portfolio_html(summary: dict) -> str:
pnl_s = "+" if summary["pnl_total"] >= 0 else ""
pnl_emoji = "🟢" if summary["pnl_total"] >= 0 else "🔴"
msg = (
f"<b>📊 Tu Cartera</b>\n\n"
f"💰 Capital: <b>{summary['capital']:.2f}€</b>\n"
f"📈 En posiciones: <b>{summary['portfolio_value']:.2f}€</b>\n"
f"🏦 Valor total: <b>{summary['total_value']:.2f}€</b>\n"
f"{pnl_emoji} P&L: <b>{pnl_s}{summary['pnl_total']:.2f}€</b> "
f"({pnl_s}{summary['pnl_pct']:.1f}%)\n"
)
if summary["positions"]:
msg += "\n<b>Posiciones abiertas:</b>\n"
for pos in summary["positions"]:
pnl_s = "+" if pos["pnl"] >= 0 else ""
emoji = "🟢" if pos["pnl"] >= 0 else "🔴"
msg += (
f"\n{emoji} <b>{h(pos['ticker'])}</b>: {pos['shares']}x\n"
f" 📍 {pos['entry_price']:.2f} → {pos['current_price']:.2f}€\n"
f" 💵 {pnl_s}{pos['pnl']:.2f}€ ({pnl_s}{pos['pnl_pct']:.1f}%)\n"
f" 🛑 SL: {pos['stop_loss']:.2f} | 🎯 TP: {pos['take_profit']:.2f}\n"
)
else:
msg += "\n<i>No hay posiciones abiertas</i>\n"
if summary["total_trades"] > 0:
wr_emoji = "✅" if summary["win_rate"] >= 0.7 else "⚠️"
msg += (
f"\n{wr_emoji} <b>Historial:</b> {summary['total_trades']} ops | "
f"{summary['winning_trades']} ganadoras | "
f"WR: {summary['win_rate']:.0%}\n"
)
t = summary["tracker"]
if t["is_paused"]:
msg += f"\n⛔ <b>Recomendador PAUSADO</b>\n<i>{h(t['pause_reason'])}</i>\n"
elif t["window_stats"]["total"] > 0:
ws = t["window_stats"]
msg += f"\n🤖 Tracker: {ws['success']}/{ws['total']} ({ws['rate']:.0%})\n"
return msg
def _currency_symbol(ticker: str) -> str:
"""Devuelve el símbolo de moneda según el ticker."""
if any(ticker.endswith(suffix) for suffix in [".MC", ".L", ".PA", ".DE", ".AS"]):
return "€"
return "$"
def format_signal_html(signal) -> str:
cur = _currency_symbol(signal.ticker)
pnl_s = "+" if signal.expected_pnl >= 0 else ""
action_emoji = "🟢 BUY" if signal.action == "BUY" else "🔴 SELL"
conf_bar = "█" * int(signal.confidence * 10) + "░" * (10 - int(signal.confidence * 10))
msg = (
f"<b>{action_emoji} {h(signal.ticker)}</b>\n\n"
f"📋 Estrategia: <code>{h(signal.strategy)}</code>\n"
f"⚡ Score: <b>{signal.score:.1f}</b>/10\n"
f"🎯 Confianza: [{conf_bar}] {signal.confidence:.0%}\n\n"
f"📍 Entrada: <b>{signal.entry_price:.2f}{cur}</b>\n"
f"🎯 Objetivo: <b>{signal.target_price:.2f}{cur}</b>\n"
f"🛑 Stop-loss: <b>{signal.stop_loss:.2f}{cur}</b>\n"
f"💵 P&L esperado: <b>{pnl_s}{signal.expected_pnl:.2f}{cur}</b>\n"
f"⚖️ R/R: <b>{signal.risk_reward:.2f}</b>\n"
)
# Mostrar tier de posición
from config import POSITION_TIER_HIGH_SCORE, POSITION_TIER_HIGH_RR
sig_score = abs(signal.score)
if sig_score >= POSITION_TIER_HIGH_SCORE and signal.risk_reward >= POSITION_TIER_HIGH_RR:
msg += f"💪 Posición: <b>30% capital</b> (señal fuerte)\n\n"
else:
msg += f"📊 Posición: <b>15% capital</b>\n\n"
msg += f"<b>Razones:</b>\n"
for r in signal.reasons:
msg += f" • {h(r)}\n"
if signal.action == "BUY":
msg += f"\n💡 <i>Para ejecutar: he comprado {h(signal.ticker)} a [precio]</i>"
else:
msg += f"\n💡 <i>Para ejecutar: he vendido {h(signal.ticker)} a [precio]</i>"
return msg
def format_analysis_html(data: dict) -> str:
"""Formatea análisis en lenguaje claro para no-expertos."""
if "error" in data:
return f"❌ {h(data['error'])}"
# Trend
trend_info = {
"ALCISTA": ("🟢", "Subiendo", "El precio lleva una racha positiva"),
"BAJISTA": ("🔴", "Bajando", "El precio lleva una racha negativa"),
"LATERAL": ("🟡", "Estable", "El precio no tiene dirección clara"),
}
trend_emoji, trend_text, trend_explain = trend_info.get(
data["trend"], ("⚪", data["trend"], "")
)
# RSI en lenguaje simple
rsi = data["rsi"]
if rsi > 75:
rsi_explain = "🔴 <b>Muy cara</b> — ha subido mucho y podría bajar pronto"
elif rsi > 60:
rsi_explain = "🟡 <b>Algo cara</b> — ha subido bastante"
elif rsi < 25:
rsi_explain = "🟢 <b>Muy barata</b> — ha caído mucho y podría rebotar"
elif rsi < 40:
rsi_explain = "🟡 <b>Algo barata</b> — ha bajado bastante"
else:
rsi_explain = "⚪ <b>Precio normal</b> — ni cara ni barata"
# Volumen
vol = data["vol_ratio"]
if vol > 2.0:
vol_explain = f"📢 <b>Mucha actividad</b> ({vol:.1f}x lo normal) — algo está pasando"
elif vol > 1.3:
vol_explain = f"📈 <b>Actividad alta</b> ({vol:.1f}x lo normal)"
elif vol < 0.5:
vol_explain = f"😴 <b>Poca actividad</b> ({vol:.1f}x lo normal) — poca gente operando"
else:
vol_explain = f"⚪ <b>Actividad normal</b> ({vol:.1f}x)"
# MACD simplificado
macd_diff = data["macd"] - data["macd_signal"]
if macd_diff > 0 and data["macd"] > 0:
macd_explain = "🟢 <b>Impulso positivo</b> — la fuerza compradora domina"
elif macd_diff < 0 and data["macd"] < 0:
macd_explain = "🔴 <b>Impulso negativo</b> — la fuerza vendedora domina"
elif macd_diff > 0:
macd_explain = "🟡 <b>Recuperándose</b> — empieza a mejorar"
else:
macd_explain = "🟡 <b>Debilitándose</b> — empieza a perder fuerza"
cur = _currency_symbol(data['ticker'])
msg = (
f"<b>📊 {h(data['ticker'])}</b>\n\n"
f"💲 Precio: <b>{data['price']:.2f}{cur}</b>\n"
f"{trend_emoji} Tendencia: <b>{trend_text}</b> — {trend_explain}\n\n"
f"<b>¿Qué dicen los indicadores?</b>\n"
f" {rsi_explain}\n"
f" {macd_explain}\n"
f" {vol_explain}\n"
)
if "sentiment" in data:
score = data["sentiment_score"]
if score > 0.1:
sent_explain = "🟢 <b>Noticias positivas</b> — el mercado habla bien"
elif score < -0.1:
sent_explain = "🔴 <b>Noticias negativas</b> — hay preocupación"
else:
sent_explain = "⚪ <b>Noticias neutrales</b> — sin novedades importantes"
msg += f"\n {sent_explain}\n"
if data.get("high_impact_news"):
msg += " ⚠️ <b>¡Hay noticias de ALTO IMPACTO!</b>\n"
if data.get("top_headlines"):
msg += "\n<b>Titulares:</b>\n"
for hl in data["top_headlines"][:3]:
msg += f" • <i>{h(hl[:80])}</i>\n"
if data.get("calendar_events"):
msg += f"\n📅 <b>Eventos próximos</b> (riesgo {h(data['calendar_risk'])}):\n"
for e in data["calendar_events"]:
msg += f" • {h(e)}\n"
return msg
def format_tracker_html(status: dict) -> str:
ws = status["window_stats"]
if status["is_paused"]:
msg = f"⛔ <b>Tracker PAUSADO</b>\n<i>Hasta: {h(str(status['paused_until']))}</i>\n<i>{h(status['pause_reason'])}</i>\n\n"
else:
msg = "✅ <b>Tracker Activo</b>\n\n"
if ws["total"] > 0:
wr = ws["rate"]
bar = "█" * int(wr * 10) + "░" * (10 - int(wr * 10))
msg += (
f"<b>Últimas {ws['total']} operaciones:</b>\n"
f" ✅ Exitosas: {ws['success']}\n"
f" ❌ Fallidas: {ws['failed']}\n"
f" [{bar}] {wr:.0%}\n\n"
)
if status["strategies"]:
msg += "<b>Estrategias:</b>\n"
for name, info in status["strategies"].items():
state_emoji = "✅" if info["active"] else "⛔"
rate_emoji = "🟢" if info["rate"] >= 0.7 else "🟡" if info["rate"] >= 0.5 else "🔴"
pnl_s = "+" if info["avg_pnl"] >= 0 else ""
msg += (
f"\n{state_emoji} <b>{h(name)}</b>\n"
f" {rate_emoji} {info['success']}/{info['total']} ({info['rate']:.0%})\n"
f" 💵 P&L medio: {pnl_s}{info['avg_pnl']:.2f}€\n"
)
return msg
# ===== Message Handler =====
async def send_html(update_or_chat, text: str, app=None):
"""Envía mensaje con HTML parsing."""
try:
if hasattr(update_or_chat, 'message'):
await update_or_chat.message.reply_text(text, parse_mode=ParseMode.HTML)
# Log outgoing
chat_id = str(update_or_chat.effective_chat.id)
clean = re.sub(r'<[^>]+>', '', text)
db.log_chat(chat_id, "bot", "out", clean[:2000])
elif app:
await app.bot.send_message(chat_id=update_or_chat, text=text, parse_mode=ParseMode.HTML)
clean = re.sub(r'<[^>]+>', '', text)
db.log_chat(str(update_or_chat), "bot", "out", clean[:2000])
except Exception as e:
# Fallback sin HTML si falla el parsing
logger.warning(f"Error enviando HTML, fallback a texto plano: {e}")
clean = re.sub(r'<[^>]+>', '', text)
if hasattr(update_or_chat, 'message'):
await update_or_chat.message.reply_text(clean)
elif app:
await app.bot.send_message(chat_id=update_or_chat, text=clean)
async def send_photo(update, photo_bytes: bytes, caption: str = ""):
"""Envía una imagen al chat."""
try:
await update.message.reply_photo(
photo=io.BytesIO(photo_bytes),
caption=caption,
parse_mode=ParseMode.HTML
)
# Guardar media y loguear
chat_id = str(update.effective_chat.id)
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
media_path = db.save_media(photo_bytes, f"chart_{ts}.png")
clean_caption = re.sub(r'<[^>]+>', '', caption) if caption else ""
db.log_chat(chat_id, "bot", "out", clean_caption, message_type="photo", media_path=media_path)
except Exception as e:
logger.error(f"Error enviando foto: {e}")
await update.message.reply_text("Error generando la imagen.")
async def _check_auth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Verifica autorización. Retorna True si autorizado."""
chat_id = str(update.effective_chat.id)
if db.is_authorized_user(chat_id):
return True
text = (update.message.text or "").strip().upper()
if text.startswith("BOLSA-") and len(text) <= 12:
user = update.effective_user
name = user.first_name or user.username or "invitado"
if db.redeem_invite_code(text, chat_id, name):
await update.message.reply_text(
f"✅ ¡Bienvenido {name}! Tu código ha sido activado.\n\n"
"Ahora recibirás las alertas del bot.\n"
"Escribe /ayuda para ver lo que puedo hacer.",
parse_mode=ParseMode.HTML
)
try:
await context.bot.send_message(
chat_id=TELEGRAM_CHAT_ID,
text=f"👤 <b>Nuevo usuario:</b> {h(name)} se ha unido con código <code>{h(text)}</code>",
parse_mode=ParseMode.HTML
)
except Exception:
pass
else:
await update.message.reply_text("❌ Código inválido o ya usado.")
else:
await update.message.reply_text(
"🔒 Bot privado. Necesitas un código de invitación.\n"
"Envía tu código aquí para activar el acceso."
)
return False
async def _analyze_full(update: Update, ticker: str):
"""Análisis completo unificado: gráfica + veredicto + explicación clara.
Combina análisis técnico, ML, sentimiento y LLM en un solo mensaje
pensado para alguien sin formación bursátil.
"""
await send_html(update, f"🔍 <i>Analizando {h(ticker)}...</i>")
# Gráfica técnica
try:
from charts import chart_technical_analysis
chart = chart_technical_analysis(ticker)
if chart:
await send_photo(update, chart, f"📈 <b>{h(ticker)}</b>")
except Exception as e:
logger.debug(f"Chart unavailable for {ticker}: {e}")
# Recoger señales para el veredicto
capital = db.get_capital()
reasons_pro = []
reasons_con = []
score_total = 0
# ML
ml_prob = 0
try:
from ml_model import predict_signal
pred = predict_signal(ticker)
if pred:
ml_prob = pred["probability"]
if ml_prob >= 0.65:
score_total += 2
reasons_pro.append(
f"La inteligencia artificial detecta oportunidad ({ml_prob:.0%} probabilidad de subida)"
)
elif ml_prob < 0.45:
score_total -= 2
reasons_con.append(
f"La inteligencia artificial no ve oportunidad ({ml_prob:.0%} probabilidad)"
)
except Exception:
pass
# Análisis técnico (estrategias)
signals = analyze_ticker(ticker, capital)
best_signal = None
if signals:
best_signal = signals[0]
if best_signal.action == "BUY" and best_signal.score >= 5:
score_total += 2
reasons_pro.append(
f"Los indicadores técnicos son positivos (puntuación {best_signal.score:.1f}/10)"
)
elif best_signal.action == "SELL" or best_signal.score < 0:
score_total -= 2
reasons_con.append(
f"Los indicadores técnicos son negativos (puntuación {best_signal.score:.1f}/10)"
)
# Datos de mercado + tendencia
data = quick_analysis(ticker)
if "error" not in data:
if data["trend"] == "ALCISTA":
score_total += 1
reasons_pro.append("La acción lleva una racha positiva (tendencia alcista)")
elif data["trend"] == "BAJISTA":
score_total -= 1
reasons_con.append("La acción lleva una racha negativa (tendencia bajista)")
if data["rsi"] > 75:
score_total -= 1
reasons_con.append(
"Ha subido demasiado rápido y podría corregir pronto (sobrecomprada)"
)
elif data["rsi"] < 30:
score_total += 1
reasons_pro.append(
"Ha caído mucho y podría rebotar pronto (sobrevendida)"
)
if data.get("sentiment") and data.get("sentiment_score", 0) < -0.1:
reasons_con.append("Las noticias recientes son negativas")
elif data.get("sentiment") and data.get("sentiment_score", 0) > 0.1:
reasons_pro.append("Las noticias recientes son positivas")
if data.get("high_impact_news"):
reasons_con.append(
"⚠️ Hay noticias de alto impacto — el precio puede moverse mucho"
)
if data.get("calendar_events"):
reasons_con.append(
f"Hay eventos económicos próximos ({data['calendar_risk']})"
)
# Veredicto claro
if score_total >= 3:
emoji = "🟢"
veredicto = "SÍ, buen momento para comprar"
explicacion = "Varios indicadores apuntan a que puede subir."
elif score_total >= 1:
emoji = "🟡"
veredicto = "Puede, pero no es la mejor entrada"
explicacion = "Hay señales mixtas. Si compras, hazlo con precaución."
elif score_total >= -1:
emoji = "🟡"
veredicto = "Mejor esperar un poco"
explicacion = "No hay suficientes señales positivas ahora mismo."
else:
emoji = "🔴"
veredicto = "NO, ahora mismo no es buena idea"
explicacion = "Los indicadores sugieren que puede seguir bajando."
# Construir mensaje principal
cur = _currency_symbol(ticker)
price = data.get("price", 0) if "error" not in data else 0
msg = f"{emoji} <b>{veredicto}</b>\n"
msg += f"<i>{explicacion}</i>\n\n"
if price:
msg += f"💲 Precio actual: <b>{price:.2f}{cur}</b>\n\n"
if reasons_pro:
msg += "<b>✅ A favor:</b>\n"
for r in reasons_pro:
msg += f" • {h(r)}\n"
msg += "\n"
if reasons_con:
msg += "<b>❌ En contra:</b>\n"
for r in reasons_con:
msg += f" • {h(r)}\n"
msg += "\n"
# Resumen de indicadores (simplificado)
if "error" not in data:
msg += format_analysis_html(data)
msg += "\n"
# Niveles de precio sugeridos
if best_signal and best_signal.action == "BUY":
msg += (
f"\n<b>📌 Si decides comprar:</b>\n"
f" 🛑 Vende si baja a <b>{best_signal.stop_loss:.2f}{cur}</b> (limitar pérdidas)\n"
f" 🎯 Objetivo de venta: <b>{best_signal.take_profit:.2f}{cur}</b>\n"
)
elif price and "error" not in data:
atr = data.get("atr", 0)
if atr > 0:
sl = price - (2.0 * atr)
tp = price + (3.0 * atr)
msg += (
f"\n<b>📌 Si decides comprar:</b>\n"
f" 🛑 Vende si baja a <b>{sl:.2f}{cur}</b> (limitar pérdidas)\n"
f" 🎯 Objetivo de venta: <b>{tp:.2f}{cur}</b>\n"
)
msg += f"\n💡 <i>Si compras, dime: he comprado {h(ticker)} a [precio]</i>"
# LLM: explicación narrativa en lenguaje sencillo
try:
from ollama import analyze_should_buy
llm_analysis = analyze_should_buy(
ticker=ticker,
score=score_total,
reasons_pro=reasons_pro,
reasons_con=reasons_con,
ml_prob=ml_prob,
trend=data.get("trend", "LATERAL") if "error" not in data else "N/A",
rsi=data.get("rsi", 50) if "error" not in data else 50,
sentiment=data.get("sentiment", "Sin datos") if "error" not in data else "Sin datos",
)
if llm_analysis:
msg += f"\n\n🧠 <b>Explicación:</b>\n<i>{h(llm_analysis)}</i>"
except Exception as e:
logger.debug(f"LLM analysis unavailable for {ticker}: {e}")
await send_html(update, msg)
async def _dispatch_intent(update: Update, context: ContextTypes.DEFAULT_TYPE, text: str):
"""Procesa un texto despachando por intent."""
intent = detect_intent(text)
try:
if intent == "greet":
await send_html(update,
"👋 <b>Hola!</b> Soy tu asistente de trading.\n\n"
"Puedo analizar acciones, gestionar tu cartera, "
"buscar oportunidades y mostrarte gráficas.\n\n"
"Escribe /ayuda o usa el menú de comandos (/) para ver todo."
)
elif intent == "thanks":
await send_html(update, "😊 De nada! Aquí estoy para lo que necesites.")
elif intent == "demo":
# Paso 1
await send_html(update,
"📖 <b>Cómo funciona el bot — Ejemplo completo</b>\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
"Imagina que estás pensando en comprar Apple.\n\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
"📝 <b>PASO 1: Preguntar</b>\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
"Tú escribes:\n"
" <code>¿compro Apple?</code>\n\n"
"El bot responde:\n\n"
"🟢 <b>SÍ, buen momento para comprar</b>\n\n"
"<b>A favor:</b>\n"
" ✅ El modelo ML dice que sí (84% probabilidad)\n"
" ✅ Tendencia alcista\n"
" ✅ Noticias positivas\n\n"
"<b>En contra:</b>\n"
" ❌ Sobrecomprado (RSI 72)\n\n"
"💲 Precio actual: <b>195.20€</b>\n"
"💡 <i>Si compras, dime: he comprado AAPL a 195</i>"
)
import asyncio
await asyncio.sleep(2)
# Paso 2
await send_html(update,
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
"📝 <b>PASO 2: Confirmar compra</b>\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
"Compras en tu broker (Trade Republic, etc).\n"
"Luego escribes:\n"
" <code>he comprado AAPL a 195</code>\n\n"
"El bot responde:\n\n"
"✅ <b>Registrado!</b>\n\n"
"📌 <b>AAPL</b> comprado a <b>195.00€</b>\n\n"
"Te avisaré:\n"
" 🛑 Vende si baja a <b>188.40€</b> (-3.4%)\n"
" 🎯 Vende si sube a <b>204.90€</b> (+5.1%)\n\n"
"<i>Reviso tu posición cada 2 horas y te aviso.</i>"
)
await asyncio.sleep(2)
# Paso 3
await send_html(update,
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
"📝 <b>PASO 3: El bot te vigila</b>\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
"Cada 2 horas recibes un mensaje:\n\n"
"🟡 <b>AAPL: +0.8%</b>\n"
"Estable. Nada que hacer por ahora.\n"
"SL en 188.40€ | TP en 204.90€\n"
"💲 Precio actual: <b>196.56€</b>\n\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
"Al día siguiente:\n\n"
"🟢 <b>AAPL: +3.2%</b>\n"
"En positivo. El trailing stop te protege.\n"
"Objetivo: 204.90€ (falta 1.9%). Aguanta.\n"
"💲 Precio actual: <b>201.24€</b>"
)
await asyncio.sleep(2)
# Paso 4
await send_html(update,
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
"📝 <b>PASO 4: El bot te avisa</b>\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
"Si las cosas van bien:\n\n"
"🟢 <b>AAPL: +5.1%</b>\n"
"Vas muy bien. Casi en objetivo (204.90€).\n"
"<b>Si quieres asegurar beneficio, puedes vender ya.</b>\n\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
"Si las cosas van mal:\n\n"
"🔴 <b>AAPL: -4.2%</b>\n"
"Muy cerca del stop-loss (188.40€).\n"
"<b>Valora vender para limitar pérdidas.</b>\n\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
"🎯 <b>Resumen: tú decides, el bot te informa.</b>\n\n"
"Si vendes, confirma: <code>he vendido AAPL a 205</code>\n\n"
"Pruébalo ahora con:\n"
" <code>¿compro Apple?</code>"
)
elif intent == "should_buy":
ticker = extract_ticker(text)
if not ticker:
await send_html(update, "❓ ¿Qué acción? Ejemplo: <b>¿compro Apple?</b>")
return
await _analyze_full(update, ticker)
elif intent == "confirmed_buy":
ticker = extract_ticker(text)
if not ticker:
await send_html(update, "❓ ¿Qué compraste? Ejemplo: <b>he comprado AAPL a 195</b>")
return
# Extraer precio del texto
import re as _re
price_match = _re.search(r'(\d+[.,]?\d*)\s*[€$]?', text.split(ticker if ticker in text.upper() else '')[-1])
if not price_match:
# Intentar con el precio del texto completo
numbers = _re.findall(r'(\d+[.,]\d+|\d{2,})', text)
if numbers:
entry_price = float(numbers[-1].replace(',', '.'))
else:
# Usar precio actual
entry_price = get_current_price(ticker)
if not entry_price:
await send_html(update, f"❓ ¿A qué precio compraste? Dime: <b>he comprado {h(ticker)} a [precio]</b>")
return
else:
entry_price = float(price_match.group(1).replace(',', '.'))
# Calcular SL/TP dinámicos
from portfolio import get_atr
atr = get_atr(ticker)
if atr and atr > 0:
stop_loss = entry_price - (2.0 * atr)
take_profit = entry_price + (3.0 * atr)
else:
stop_loss = entry_price * 0.95
take_profit = entry_price * 1.10
# Registrar en DB
pos_id = db.open_position(
ticker=ticker, shares=1, entry_price=entry_price,
stop_loss=stop_loss, take_profit=take_profit,
commission=0 # Comisión real ya la paga el usuario en su broker
)
sl_pct = ((stop_loss - entry_price) / entry_price) * 100
tp_pct = ((take_profit - entry_price) / entry_price) * 100
cur = _currency_symbol(ticker)
msg = (
f"✅ <b>Registrado!</b>\n\n"
f"📌 <b>{h(ticker)}</b> comprado a <b>{entry_price:.2f}{cur}</b>\n\n"
f"Te avisaré:\n"
f" 🛑 Vende si baja a <b>{stop_loss:.2f}{cur}</b> ({sl_pct:.1f}%)\n"
f" 🎯 Vende si sube a <b>{take_profit:.2f}{cur}</b> (+{tp_pct:.1f}%)\n\n"
f"<i>Reviso tu posición cada 2 horas y te aviso.</i>"
)
await send_html(update, msg)
elif intent == "confirmed_sell":
ticker = extract_ticker(text)
if not ticker:
await send_html(update, "❓ ¿Qué vendiste? Ejemplo: <b>he vendido AAPL a 200</b>")
return
# Extraer precio del texto
import re as _re
price_match = _re.search(r'(\d+[.,]?\d*)\s*[€$]?', text.split(ticker if ticker in text.upper() else '')[-1])
if not price_match:
numbers = _re.findall(r'(\d+[.,]\d+|\d{2,})', text)
if numbers:
sell_price = float(numbers[-1].replace(',', '.'))
else:
sell_price = get_current_price(ticker)
if not sell_price:
await send_html(update, f"❓ ¿A qué precio vendiste? Dime: <b>he vendido {h(ticker)} a [precio]</b>")
return
else:
sell_price = float(price_match.group(1).replace(',', '.'))
# Buscar posición abierta
pos = db.get_position_by_ticker(ticker)
if not pos:
await send_html(update, f"❌ No tengo registrada una posición abierta en <b>{h(ticker)}</b>")
return
entry_price = pos["entry_price"]
shares = pos["shares"]
pnl = (sell_price - entry_price) * shares
pnl_pct = ((sell_price - entry_price) / entry_price) * 100
pnl_emoji = "🟢" if pnl >= 0 else "🔴"
pnl_sign = "+" if pnl >= 0 else ""
db.close_position(pos["id"], sell_price, pnl)
cur = _currency_symbol(ticker)
msg = (
f"{pnl_emoji} <b>{h(ticker)}</b>\n\n"
f"📍 {entry_price:.2f} → {sell_price:.2f}{cur}\n"
f"💵 {pnl_sign}{pnl:.2f}{cur} ({pnl_sign}{pnl_pct:.1f}%)\n\n"
f"<i>Resultado registrado. Esto me ayuda a mejorar.</i>"
)
await send_html(update, msg)
elif intent == "help":
await send_html(update,
"<b>📖 Qué puedo hacer:</b>\n\n"
"<b>🚦 Lo básico</b>\n"
" /ejemplo → Tutorial paso a paso\n"
" <code>¿compro Apple?</code> → Semáforo 🟢🟡🔴\n"
" <code>he comprado AAPL a 195</code> → Registro + alertas\n"
" <code>he vendido AAPL a 200</code> → Cierre + P&L\n\n"
"<b>📊 Mi cartera</b>\n"
" /portfolio → Posiciones abiertas\n"
" /historial → Operaciones cerradas\n"
" /revista → Análisis: vender/mantener/añadir\n"
" <code>optimiza</code> → Sugerir rotación\n\n"
"<b>🔍 Mercado</b>\n"
" /escanear → Buscar oportunidades\n"
" /mercado → Heatmap general\n"
" /analiza <code>AAPL</code> → Análisis técnico\n"
" /noticias <code>AAPL</code> → Sentimiento\n"
" /calendario → Eventos económicos\n"
" <code>compara AAPL MSFT</code> → Comparativa\n\n"
"<b>🔬 Avanzado</b>\n"
" /backtest <code>AAPL</code> → Simular estrategias\n"
" <code>predice AAPL</code> → Predicción ML\n"
" /regimen → Estado mercado (bull/bear)\n"
" <code>momentum</code> → Rotación ETFs mensual\n"
" <code>earnings</code> → Oportunidades PEAD\n\n"
"<b>⚙️ Gestión</b>\n"
" /watchlist → Ver lista\n"
" <code>añade AAPL</code> · <code>quita AAPL</code>\n"
" /alertas → Alertas activas\n"
" <code>avísame cuando AAPL</code> → Nueva alerta\n"
" /tracker → Precisión del bot\n\n"
"<b>🏦 Trade Republic</b>\n"
" <code>tr conecta</code> · <code>tr portfolio</code> · <code>sincroniza</code>\n\n"
"<b>👥 Usuarios</b> (admin)\n"
" <code>invitar Eduardo</code> · <code>usuarios</code> · <code>eliminar usuario</code>\n\n"
"💬 <i>Usa el menú / o escríbeme en lenguaje natural.</i>"
)
elif intent == "guide":
# Parte 1: Qué es y cómo funciona
await send_html(update,
"📖 <b>GUÍA DEL BOT DE TRADING</b>\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
"🤖 <b>¿Qué es?</b>\n"
"Un bot que analiza acciones del mercado americano y europeo "
"usando análisis técnico + inteligencia artificial. "
"Te dice cuándo comprar, cuándo vender, y vigila tus posiciones "
"para que no tengas que estar mirando la pantalla.\n\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
"🚦 <b>EL SEMÁFORO</b>\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
"Preguntas <i>\"¿compro Apple?\"</i> y el bot responde:\n\n"
"🟢 <b>SÍ</b> — Buen momento, las métricas están alineadas\n"
"🟡 <b>PUEDE</b> — No es ideal, mejor esperar\n"
"🔴 <b>NO</b> — Ahora mismo es mala idea\n\n"
"Si decides comprar, dices <i>\"he comprado AAPL a 195\"</i> "
"y el bot registra tu posición. A partir de ahí te vigila "
"automáticamente y te avisa si tienes que vender.\n\n"
"Escribe <b>ejemplo</b> para ver el flujo completo paso a paso."
)
# Parte 2: Estrategias
await send_html(update,
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
"📊 <b>LAS 4 ESTRATEGIAS</b>\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"