mtcli 3.8.0.dev11__tar.gz → 3.8.0.dev12__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/PKG-INFO +1 -1
  2. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/cli.py +8 -4
  3. mtcli-3.8.0.dev12/mtcli/commands/backfill.py +68 -0
  4. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/commands/ticks.py +9 -12
  5. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/database.py +12 -22
  6. mtcli-3.8.0.dev12/mtcli/marketdata/backfill_engine.py +201 -0
  7. mtcli-3.8.0.dev12/mtcli/marketdata/tick_bus.py +45 -0
  8. mtcli-3.8.0.dev12/mtcli/marketdata/tick_cache.py +160 -0
  9. mtcli-3.8.0.dev12/mtcli/marketdata/tick_engine.py +151 -0
  10. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/marketdata/tick_repository.py +127 -32
  11. mtcli-3.8.0.dev12/mtcli/marketdata/tick_writer.py +38 -0
  12. mtcli-3.8.0.dev12/mtcli/services/__init__.py +0 -0
  13. mtcli-3.8.0.dev12/mtcli/services/maintenance_service.py +170 -0
  14. mtcli-3.8.0.dev12/mtcli/services/tick_service.py +82 -0
  15. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/pyproject.toml +1 -1
  16. mtcli-3.8.0.dev11/mtcli/marketdata/tick_cache.py +0 -61
  17. mtcli-3.8.0.dev11/mtcli/marketdata/tick_engine.py +0 -162
  18. mtcli-3.8.0.dev11/mtcli/marketdata/tick_writer.py +0 -98
  19. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/LICENSE +0 -0
  20. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/README.md +0 -0
  21. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/__init__.py +0 -0
  22. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/__main__.py +0 -0
  23. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/cli_dev.py +0 -0
  24. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/commands/__init__.py +0 -0
  25. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/commands/bars.py +0 -0
  26. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/commands/conf.py +0 -0
  27. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/commands/doctor.py +0 -0
  28. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/commands_dev/__init__.py +0 -0
  29. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/commands_dev/migrate.py +0 -0
  30. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/conecta.py +0 -0
  31. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/conf.py +0 -0
  32. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/config_registre.py +0 -0
  33. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/data/__init__.py +0 -0
  34. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/data/base.py +0 -0
  35. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/data/csv.py +0 -0
  36. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/data/mt5.py +0 -0
  37. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/domain/__init__.py +0 -0
  38. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/domain/timeframe.py +0 -0
  39. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/logger.py +0 -0
  40. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/marketdata/__init__.py +0 -0
  41. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/migrations/001_initial_schema.py +0 -0
  42. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/migrations/002_ticks_time_msc.py +0 -0
  43. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/migrations/003_optimize_ticks_without_rowid.py +0 -0
  44. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/migrations/004_ticks_pk_time_msc.py +0 -0
  45. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/migrations/005_tick_price_compression.py +0 -0
  46. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/migrations/006_add_index_ticks_symbol_time.py +0 -0
  47. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/migrations/007_deduplicate_ticks_and_add_unique_index.py +0 -0
  48. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/migrations/__init__.py +0 -0
  49. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/migrations/__main__.py +0 -0
  50. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/migrations/runner.py +0 -0
  51. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/models/__init__.py +0 -0
  52. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/models/bar_model.py +0 -0
  53. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/models/bars_model.py +0 -0
  54. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/models/chart_model.py +0 -0
  55. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/models/conf_model.py +0 -0
  56. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/models/consecutive_bars_model.py +0 -0
  57. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/models/rates_model.py +0 -0
  58. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/models/signals_model.py +0 -0
  59. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/models/unconsecutive_bar_model.py +0 -0
  60. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/mt5_context.py +0 -0
  61. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugin.py +0 -0
  62. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugin_loader.py +0 -0
  63. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugin_manager.py +0 -0
  64. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/__init__.py +0 -0
  65. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/exemplo.py-dist +0 -0
  66. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/media_movel/__init__.py +0 -0
  67. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/media_movel/cli.py +0 -0
  68. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/media_movel/conf.py +0 -0
  69. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/media_movel/models/__init__.py +0 -0
  70. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/media_movel/models/model_media_movel.py +0 -0
  71. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/media_movel/tests/__init__.py +0 -0
  72. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/media_movel/tests/test_mm.py +0 -0
  73. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/media_movel/tests/test_model_media_movel.py +0 -0
  74. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/range_medio/__init__.py +0 -0
  75. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/range_medio/cli.py +0 -0
  76. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/range_medio/conf.py +0 -0
  77. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/range_medio/models/__init__.py +0 -0
  78. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/range_medio/models/average_range_model.py +0 -0
  79. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/range_medio/tests/__init__.py +0 -0
  80. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/range_medio/tests/test_rm.py +0 -0
  81. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/volume_medio/__init__.py +0 -0
  82. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/volume_medio/cli.py +0 -0
  83. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/volume_medio/conf.py +0 -0
  84. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/volume_medio/models/__init__.py +0 -0
  85. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/volume_medio/models/model_average_volume.py +0 -0
  86. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/volume_medio/tests/__init__.py +0 -0
  87. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/plugins/volume_medio/tests/test_vm.py +0 -0
  88. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/views/__init__.py +0 -0
  89. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/views/close_view.py +0 -0
  90. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/views/full_view.py +0 -0
  91. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/views/high_view.py +0 -0
  92. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/views/low_view.py +0 -0
  93. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/views/min_view.py +0 -0
  94. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/views/open_view.py +0 -0
  95. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/views/ranges_view.py +0 -0
  96. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/views/rates_view.py +0 -0
  97. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/views/vars_view.py +0 -0
  98. {mtcli-3.8.0.dev11 → mtcli-3.8.0.dev12}/mtcli/views/volumes_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mtcli
3
- Version: 3.8.0.dev11
3
+ Version: 3.8.0.dev12
4
4
  Summary: Aplicativo CLI para exibir gráficos do MetaTrader 5 screen reader friendly
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -23,11 +23,12 @@ import click
23
23
 
24
24
  from mtcli.plugin_loader import load_plugins
25
25
  from mtcli.logger import setup_logger
26
- from mtcli.marketdata.tick_engine import TickEngine
26
+ from mtcli.services.tick_service import ensure_tick_engine
27
27
 
28
28
  from .commands.bars import bars
29
29
  from .commands.doctor import doctor
30
30
  from .commands.ticks import ticks
31
+ from .commands.backfill import backfill
31
32
 
32
33
 
33
34
  logger = setup_logger(__name__)
@@ -55,15 +56,17 @@ def start_tick_capture():
55
56
  symbol = os.getenv("MTCLI_SYMBOL")
56
57
 
57
58
  if not symbol:
58
- logger.info("Captura automática de ticks desativada (MTCLI_SYMBOL não definido).")
59
+ logger.info(
60
+ "Captura automática de ticks desativada (MTCLI_SYMBOL não definido)."
61
+ )
59
62
  return
60
63
 
61
64
  logger.info("Iniciando captura contínua de ticks para %s", symbol)
62
65
 
63
66
  try:
64
67
 
65
- _tick_engine = TickEngine([symbol])
66
- _tick_engine.start()
68
+ # CORREÇÃO PRINCIPAL
69
+ _tick_engine = ensure_tick_engine([symbol])
67
70
 
68
71
  logger.info("Captura de ticks iniciada em background.")
69
72
 
@@ -89,6 +92,7 @@ mt.add_command(doctor, name="doctor")
89
92
  mt.add_command(bars, name="bars")
90
93
  mt.add_command(doctor, name="dr")
91
94
  mt.add_command(ticks)
95
+ mt.add_command(backfill, name="fill")
92
96
 
93
97
  loaded_plugins = load_plugins(mt)
94
98
 
@@ -0,0 +1,68 @@
1
+ """
2
+ Comando CLI: backfill
3
+
4
+ Responsável por carregar histórico de ticks do MetaTrader5
5
+ para o banco SQLite do mtcli utilizando o BackfillEngine.
6
+
7
+ Fluxo:
8
+
9
+ BackfillEngine
10
+
11
+ TickRepository
12
+
13
+ SQLite
14
+
15
+ Opcionalmente os ticks também podem ser publicados no TickBus
16
+ para que plugins consumam o fluxo histórico.
17
+ """
18
+
19
+ import click
20
+
21
+ from mtcli.marketdata.backfill_engine import BackfillEngine
22
+ from mtcli.marketdata.tick_bus import TickBus
23
+ from mtcli.marketdata.tick_repository import TickRepository
24
+
25
+
26
+ @click.command()
27
+ @click.argument("symbol")
28
+ @click.option(
29
+ "--days",
30
+ default=5,
31
+ show_default=True,
32
+ help="Número de dias de histórico a carregar caso não exista histórico local.",
33
+ )
34
+ def backfill(symbol: str, days: int):
35
+ """
36
+ Executa backfill histórico de ticks.
37
+
38
+ Este comando baixa ticks históricos diretamente do MetaTrader5
39
+ e os grava no banco local SQLite do mtcli.
40
+
41
+ O processo é incremental:
42
+
43
+ - se já existirem ticks no banco, o backfill continua do último tick
44
+ - caso contrário, carrega o número de dias definido em --days
45
+
46
+ Examples
47
+ --------
48
+
49
+ Carregar 5 dias:
50
+
51
+ mt fill WINJ26
52
+
53
+ Carregar 30 dias:
54
+
55
+ mt fill WINJ26 --days 30
56
+ """
57
+
58
+ # Event bus (permite que plugins consumam os ticks históricos)
59
+ bus = TickBus()
60
+
61
+ # Repositório de persistência
62
+ repo = TickRepository()
63
+
64
+ # Engine de backfill
65
+ engine = BackfillEngine(symbol, bus, repo)
66
+
67
+ # Executa o carregamento histórico
68
+ engine.run(days)
@@ -1,45 +1,42 @@
1
1
  """
2
2
  Comando CLI para captura contínua de ticks.
3
3
 
4
- Permite iniciar um TickEngine manualmente
4
+ Permite iniciar o serviço de captura de ticks
5
5
  para um ou mais símbolos.
6
6
 
7
7
  Exemplo:
8
8
 
9
9
  mt ticks WIN$N
10
10
  mt ticks WIN$N WDO$N PETR4
11
-
12
- A captura continua até o usuário interromper
13
- com Ctrl+C.
14
11
  """
15
12
 
16
13
  import time
17
14
  import click
18
15
 
19
- from mtcli.marketdata.tick_engine import TickEngine
16
+ from mtcli.services.tick_service import ensure_tick_engine
20
17
 
21
18
 
22
19
  @click.command()
23
20
  @click.argument("symbols", nargs=-1)
24
21
  def ticks(symbols):
25
22
  """
26
- Inicia captura contínua de ticks para os símbolos informados.
23
+ Inicia captura contínua de ticks.
27
24
  """
28
25
 
29
26
  if not symbols:
30
27
  click.echo("Informe ao menos um símbolo.")
31
28
  return
32
29
 
33
- engine = TickEngine(symbols)
30
+ # atualmente o engine suporta apenas 1 símbolo
31
+ symbol = symbols[0]
34
32
 
35
- click.echo("Iniciando captura de ticks...")
33
+ engine = ensure_tick_engine(symbol)
36
34
 
37
- engine.start()
35
+ click.echo(f"Captura de ticks iniciada para {symbol}")
38
36
 
39
37
  try:
40
-
41
- while True:
42
- time.sleep(1)
38
+ # inicia engine
39
+ engine.start()
43
40
 
44
41
  except KeyboardInterrupt:
45
42
 
@@ -3,17 +3,18 @@ Core de acesso ao banco SQLite do mtcli.
3
3
 
4
4
  Responsável por:
5
5
 
6
- - Criar conexão SQLite
7
- - Aplicar otimizações de performance
8
- - Ativar WAL
9
- - Gerenciar migrations
10
- - Backup e manutenção do banco
6
+ - criar conexão SQLite
7
+ - aplicar otimizações
8
+ - executar migrations automaticamente
9
+ - backup automático
11
10
  """
12
11
 
13
12
  import sqlite3
14
13
  from pathlib import Path
15
14
  from datetime import datetime
15
+
16
16
  from .conf import DB_NAME
17
+ from .migrations.runner import run_migrations
17
18
 
18
19
 
19
20
  DB_PATH = Path.home() / ".mtcli" / DB_NAME
@@ -24,8 +25,7 @@ _connection = None
24
25
 
25
26
  def get_connection():
26
27
  """
27
- Retorna conexão singleton SQLite otimizada para ingestão
28
- contínua de ticks.
28
+ Retorna conexão singleton SQLite.
29
29
  """
30
30
 
31
31
  global _connection
@@ -38,25 +38,18 @@ def get_connection():
38
38
 
39
39
  conn = sqlite3.connect(DB_PATH, check_same_thread=False)
40
40
 
41
+ # otimizações
41
42
  conn.execute("PRAGMA journal_mode=WAL")
42
43
  conn.execute("PRAGMA synchronous=NORMAL")
43
44
  conn.execute("PRAGMA temp_store=MEMORY")
44
45
  conn.execute("PRAGMA mmap_size=30000000000")
45
46
  conn.execute("PRAGMA cache_size=-200000")
46
47
  conn.execute("PRAGMA journal_size_limit=67108864")
47
- conn.execute("PRAGMA read_uncommitted = TRUE")
48
-
49
- # checkpoint automático
50
- conn.execute("PRAGMA wal_autocheckpoint=1000")
51
-
52
- conn.execute("""
53
- CREATE TABLE IF NOT EXISTS schema_migrations(
54
- version INTEGER PRIMARY KEY,
55
- applied_at TEXT NOT NULL
56
- )
57
- """)
48
+ conn.execute("PRAGMA wal_autocheckpoint=5000")
49
+ conn.execute("PRAGMA foreign_keys=ON")
58
50
 
59
- conn.commit()
51
+ # executa migrations automaticamente
52
+ run_migrations(conn)
60
53
 
61
54
  _connection = conn
62
55
 
@@ -68,9 +61,6 @@ def get_connection():
68
61
  # ==========================================================
69
62
 
70
63
  def backup_database(conn):
71
- """
72
- Realiza backup diário seguro do banco SQLite.
73
- """
74
64
 
75
65
  now = datetime.now().strftime("%Y%m%d")
76
66
 
@@ -0,0 +1,201 @@
1
+ """
2
+ BackfillEngine
3
+
4
+ Carrega ticks históricos do MetaTrader5 de forma eficiente
5
+ e os publica no TickBus para processamento.
6
+
7
+ Características:
8
+
9
+ • leitura em chunks grandes (100k ticks)
10
+ • proteção contra duplicação
11
+ • compatível com TickBus
12
+ • inserção em batch no SQLite
13
+ • sem barra de progresso (CLI acessível)
14
+ • baixo uso de memória
15
+ """
16
+
17
+ import datetime
18
+ import MetaTrader5 as mt5
19
+
20
+ from mtcli.logger import setup_logger
21
+
22
+ logger = setup_logger(__name__)
23
+
24
+
25
+ class BackfillEngine:
26
+ """
27
+ Engine responsável pelo carregamento histórico de ticks.
28
+
29
+ O BackfillEngine baixa ticks históricos diretamente do MT5
30
+ utilizando janelas grandes e publica os ticks no TickBus,
31
+ permitindo que múltiplos subscribers processem os dados.
32
+
33
+ Tipicamente os subscribers incluem:
34
+
35
+ - TickWriter (persistência em banco)
36
+ - plugins de análise
37
+ - geradores de estruturas derivadas (Renko, bars, etc)
38
+
39
+ Fluxo arquitetural:
40
+
41
+ MetaTrader5
42
+
43
+ BackfillEngine
44
+
45
+ TickBus
46
+
47
+ Subscribers
48
+
49
+ SQLite / Plugins
50
+ """
51
+
52
+ CHUNK_SIZE = 100_000
53
+
54
+ def __init__(self, symbol, tick_bus, repository):
55
+ """
56
+ Parameters
57
+ ----------
58
+ symbol : str
59
+ Símbolo a carregar (ex: WINJ26)
60
+
61
+ tick_bus : TickBus
62
+ Event bus responsável pela distribuição dos ticks.
63
+
64
+ repository : TickRepository
65
+ Repositório responsável pela persistência.
66
+ """
67
+
68
+ self.symbol = symbol
69
+ self.tick_bus = tick_bus
70
+ self.repository = repository
71
+
72
+ self.last_time_msc = None
73
+
74
+ # ---------------------------------------------------------
75
+ # util
76
+ # ---------------------------------------------------------
77
+
78
+ def _get_last_stored(self):
79
+ """
80
+ Consulta o último tick armazenado no banco.
81
+
82
+ Returns
83
+ -------
84
+ int or None
85
+ Timestamp em milissegundos do último tick armazenado.
86
+ """
87
+
88
+ last = self.repository._get_last_tick_msc(self.symbol)
89
+
90
+ if last:
91
+ logger.info(
92
+ "Backfill retomando do último tick armazenado: %s",
93
+ last,
94
+ )
95
+
96
+ return last
97
+
98
+ # ---------------------------------------------------------
99
+ # main
100
+ # ---------------------------------------------------------
101
+
102
+ def run(self, days=5):
103
+ """
104
+ Executa o processo de backfill.
105
+
106
+ Parameters
107
+ ----------
108
+ days : int
109
+ Número de dias de histórico caso não exista
110
+ histórico local no banco.
111
+
112
+ Notes
113
+ -----
114
+ O processo é incremental:
115
+
116
+ - se o banco possuir ticks, continua a partir do último
117
+ - caso contrário inicia no passado definido por `days`
118
+ """
119
+
120
+ logger.info(
121
+ "Backfill iniciado (%s) — até %s dias de histórico",
122
+ self.symbol,
123
+ days,
124
+ )
125
+
126
+ if not mt5.initialize():
127
+ logger.error("Falha ao inicializar MetaTrader5")
128
+ return
129
+
130
+ mt5.symbol_select(self.symbol, True)
131
+
132
+ now = datetime.datetime.utcnow()
133
+
134
+ last = self._get_last_stored()
135
+
136
+ if last:
137
+ start = datetime.datetime.fromtimestamp(last / 1000)
138
+ self.last_time_msc = last
139
+ else:
140
+ start = now - datetime.timedelta(days=days)
141
+
142
+ total_loaded = 0
143
+
144
+ while True:
145
+
146
+ ticks = mt5.copy_ticks_from(
147
+ self.symbol,
148
+ start,
149
+ self.CHUNK_SIZE,
150
+ mt5.COPY_TICKS_ALL,
151
+ )
152
+
153
+ if ticks is None or len(ticks) == 0:
154
+ break
155
+
156
+ # ---------------------------------------------
157
+ # filtro de duplicação (vetorizado numpy)
158
+ # ---------------------------------------------
159
+
160
+ if self.last_time_msc:
161
+
162
+ mask = ticks["time_msc"] > self.last_time_msc
163
+ ticks = ticks[mask]
164
+
165
+ if len(ticks) == 0:
166
+ break
167
+
168
+ # ---------------------------------------------
169
+ # publica no event bus
170
+ # ---------------------------------------------
171
+
172
+ for i in range(len(ticks)):
173
+ self.tick_bus.publish(ticks[i])
174
+
175
+ # ---------------------------------------------
176
+ # grava no banco
177
+ # ---------------------------------------------
178
+
179
+ inserted = self.repository.insert_ticks(
180
+ self.symbol,
181
+ ticks
182
+ )
183
+
184
+ total_loaded += inserted
185
+
186
+ # último tick do chunk
187
+ self.last_time_msc = int(ticks["time_msc"][-1])
188
+
189
+ # próximo ponto de leitura
190
+ start = datetime.datetime.fromtimestamp(
191
+ self.last_time_msc / 1000
192
+ )
193
+
194
+ if len(ticks) < self.CHUNK_SIZE:
195
+ break
196
+
197
+ logger.info(
198
+ "Backfill concluído (%s) — %s ticks inseridos",
199
+ self.symbol,
200
+ f"{total_loaded:,}",
201
+ )
@@ -0,0 +1,45 @@
1
+ """
2
+ TickBus
3
+
4
+ Event Bus simples para distribuição de ticks.
5
+ """
6
+
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class TickBus:
13
+ """
14
+ Implementa um Event Bus simples para ticks.
15
+ """
16
+
17
+ def __init__(self):
18
+ self.subscribers = []
19
+
20
+ # ---------------------------------------------------------
21
+
22
+ def subscribe(self, handler):
23
+ """
24
+ Registra um subscriber para receber ticks.
25
+ """
26
+
27
+ self.subscribers.append(handler)
28
+
29
+ name = getattr(handler, "__qualname__", handler.__class__.__name__)
30
+
31
+ logger.debug("Subscriber registrado: %s", name)
32
+
33
+ # ---------------------------------------------------------
34
+
35
+ def publish(self, tick):
36
+ """
37
+ Publica um tick para todos os subscribers.
38
+ """
39
+
40
+ for handler in self.subscribers:
41
+ try:
42
+ handler(tick)
43
+
44
+ except Exception:
45
+ logger.exception("Erro em subscriber")
@@ -0,0 +1,160 @@
1
+ """
2
+ Cache de ticks em memória.
3
+
4
+ Mantém uma janela recente de ticks para acesso rápido
5
+ sem necessidade de consultar o banco SQLite.
6
+
7
+ Este cache é usado principalmente por:
8
+
9
+ - plugins em tempo real
10
+ - cálculos de indicadores
11
+ - geração de renko
12
+ - tape reading
13
+ - consultas recentes
14
+
15
+ A estrutura utiliza `collections.deque`, que oferece:
16
+
17
+ - inserção O(1)
18
+ - remoção automática quando atinge max_size
19
+ - excelente performance para streams de dados
20
+ """
21
+
22
+ from collections import deque
23
+ from typing import Iterable, Iterator, Any, List
24
+
25
+
26
+ class TickCache:
27
+ """
28
+ Mantém uma janela deslizante de ticks em memória.
29
+
30
+ Parameters
31
+ ----------
32
+ max_size : int
33
+ Número máximo de ticks armazenados no cache.
34
+ """
35
+
36
+ def __init__(self, max_size: int = 10000):
37
+
38
+ self.buffer: deque = deque(maxlen=max_size)
39
+
40
+ # ==========================================================
41
+ # INSERÇÃO
42
+ # ==========================================================
43
+
44
+ def add(self, tick: Any) -> None:
45
+ """
46
+ Adiciona um único tick ao cache.
47
+
48
+ Parameters
49
+ ----------
50
+ tick : Any
51
+ Tick a ser armazenado.
52
+ """
53
+
54
+ self.buffer.append(tick)
55
+
56
+ # ----------------------------------------------------------
57
+
58
+ def add_many(self, ticks: Iterable[Any]) -> None:
59
+ """
60
+ Adiciona múltiplos ticks ao cache.
61
+
62
+ Parameters
63
+ ----------
64
+ ticks : iterable
65
+ Coleção de ticks.
66
+ """
67
+
68
+ for t in ticks:
69
+ self.buffer.append(t)
70
+
71
+ # ==========================================================
72
+ # CONSULTA
73
+ # ==========================================================
74
+
75
+ def get_last(self) -> Any:
76
+ """
77
+ Retorna o último tick armazenado.
78
+
79
+ Returns
80
+ -------
81
+ tick ou None
82
+ """
83
+
84
+ if self.buffer:
85
+ return self.buffer[-1]
86
+
87
+ return None
88
+
89
+ # ----------------------------------------------------------
90
+
91
+ def get_last_n(self, n: int) -> List[Any]:
92
+ """
93
+ Retorna os últimos N ticks do cache.
94
+
95
+ Parameters
96
+ ----------
97
+ n : int
98
+ Número de ticks desejados.
99
+
100
+ Returns
101
+ -------
102
+ list
103
+ Lista contendo os últimos ticks.
104
+ """
105
+
106
+ if n <= 0:
107
+ return []
108
+
109
+ return list(self.buffer)[-n:]
110
+
111
+ # ----------------------------------------------------------
112
+
113
+ def get_all(self) -> List[Any]:
114
+ """
115
+ Retorna todos os ticks armazenados no cache.
116
+
117
+ Returns
118
+ -------
119
+ list
120
+ """
121
+
122
+ return list(self.buffer)
123
+
124
+ # ==========================================================
125
+ # UTILIDADES
126
+ # ==========================================================
127
+
128
+ def clear(self) -> None:
129
+ """
130
+ Limpa completamente o cache.
131
+ """
132
+
133
+ self.buffer.clear()
134
+
135
+ # ----------------------------------------------------------
136
+
137
+ def __len__(self) -> int:
138
+ """
139
+ Retorna o número atual de ticks armazenados.
140
+
141
+ Permite uso de:
142
+
143
+ len(cache)
144
+ """
145
+
146
+ return len(self.buffer)
147
+
148
+ # ----------------------------------------------------------
149
+
150
+ def __iter__(self) -> Iterator[Any]:
151
+ """
152
+ Permite iterar diretamente sobre o cache.
153
+
154
+ Exemplo:
155
+
156
+ for tick in cache:
157
+ ...
158
+ """
159
+
160
+ return iter(self.buffer)