mtcli 3.8.0.dev3__tar.gz → 3.8.0.dev5__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 (83) hide show
  1. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/PKG-INFO +1 -1
  2. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/cli.py +31 -19
  3. mtcli-3.8.0.dev5/mtcli/commands/ticks.py +47 -0
  4. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/database.py +14 -32
  5. mtcli-3.8.0.dev5/mtcli/marketdata/tick_engine.py +134 -0
  6. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/marketdata/tick_repository.py +52 -60
  7. mtcli-3.8.0.dev5/mtcli/migrations/004_ticks_pk_time_msc.py +56 -0
  8. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/pyproject.toml +1 -1
  9. mtcli-3.8.0.dev3/mtcli/marketdata/tick_engine.py +0 -93
  10. mtcli-3.8.0.dev3/mtcli/marketdata/tick_streamer.py +0 -75
  11. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/LICENSE +0 -0
  12. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/README.md +0 -0
  13. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/__init__.py +0 -0
  14. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/commands/__init__.py +0 -0
  15. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/commands/bars.py +0 -0
  16. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/commands/conf.py +0 -0
  17. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/commands/doctor.py +0 -0
  18. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/commands/migrate.py +0 -0
  19. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/conecta.py +0 -0
  20. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/conf.py +0 -0
  21. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/config_registre.py +0 -0
  22. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/data/__init__.py +0 -0
  23. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/data/base.py +0 -0
  24. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/data/csv.py +0 -0
  25. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/data/mt5.py +0 -0
  26. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/domain/__init__.py +0 -0
  27. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/domain/timeframe.py +0 -0
  28. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/logger.py +0 -0
  29. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/marketdata/__init__.py +0 -0
  30. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/marketdata/tick_cache.py +0 -0
  31. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/migrations/001_initial_schema.py +0 -0
  32. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/migrations/002_ticks_time_msc.py +0 -0
  33. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/migrations/003_optimize_ticks_without_rowid.py +0 -0
  34. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/migrations/__init__.py +0 -0
  35. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/migrations/runner.py +0 -0
  36. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/models/__init__.py +0 -0
  37. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/models/bar_model.py +0 -0
  38. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/models/bars_model.py +0 -0
  39. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/models/chart_model.py +0 -0
  40. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/models/conf_model.py +0 -0
  41. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/models/consecutive_bars_model.py +0 -0
  42. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/models/rates_model.py +0 -0
  43. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/models/signals_model.py +0 -0
  44. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/models/unconsecutive_bar_model.py +0 -0
  45. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/mt5_context.py +0 -0
  46. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugin.py +0 -0
  47. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugin_loader.py +0 -0
  48. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugin_manager.py +0 -0
  49. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/__init__.py +0 -0
  50. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/exemplo.py-dist +0 -0
  51. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/media_movel/__init__.py +0 -0
  52. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/media_movel/cli.py +0 -0
  53. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/media_movel/conf.py +0 -0
  54. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/media_movel/models/__init__.py +0 -0
  55. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/media_movel/models/model_media_movel.py +0 -0
  56. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/media_movel/tests/__init__.py +0 -0
  57. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/media_movel/tests/test_mm.py +0 -0
  58. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/media_movel/tests/test_model_media_movel.py +0 -0
  59. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/range_medio/__init__.py +0 -0
  60. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/range_medio/cli.py +0 -0
  61. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/range_medio/conf.py +0 -0
  62. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/range_medio/models/__init__.py +0 -0
  63. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/range_medio/models/average_range_model.py +0 -0
  64. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/range_medio/tests/__init__.py +0 -0
  65. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/range_medio/tests/test_rm.py +0 -0
  66. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/volume_medio/__init__.py +0 -0
  67. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/volume_medio/cli.py +0 -0
  68. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/volume_medio/conf.py +0 -0
  69. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/volume_medio/models/__init__.py +0 -0
  70. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/volume_medio/models/model_average_volume.py +0 -0
  71. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/volume_medio/tests/__init__.py +0 -0
  72. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/volume_medio/tests/test_vm.py +0 -0
  73. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/__init__.py +0 -0
  74. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/close_view.py +0 -0
  75. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/full_view.py +0 -0
  76. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/high_view.py +0 -0
  77. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/low_view.py +0 -0
  78. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/min_view.py +0 -0
  79. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/open_view.py +0 -0
  80. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/ranges_view.py +0 -0
  81. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/rates_view.py +0 -0
  82. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/vars_view.py +0 -0
  83. {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/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.dev3
3
+ Version: 3.8.0.dev5
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
@@ -1,63 +1,74 @@
1
1
  """
2
2
  CLI principal do mtcli.
3
+
4
+ Responsável por:
5
+
6
+ - inicializar o ambiente do CLI
7
+ - carregar plugins
8
+ - iniciar captura automática de ticks (opcional)
9
+
10
+ A captura automática é ativada se a variável de ambiente
11
+ MTCLI_SYMBOL estiver definida.
12
+
13
+ Exemplo:
14
+
15
+ MTCLI_SYMBOL=WIN$N mt bars
16
+
17
+ Nesse caso o mtcli inicia um TickEngine em background
18
+ para manter o histórico de ticks atualizado.
3
19
  """
4
20
 
5
21
  import os
6
- import threading
7
22
  import click
8
23
 
9
24
  from mtcli.plugin_loader import load_plugins
10
25
  from mtcli.logger import setup_logger
11
- from mtcli.marketdata.tick_streamer import TickStreamer
26
+ from mtcli.marketdata.tick_engine import TickEngine
12
27
 
13
28
  from .commands.bars import bars
14
29
  from .commands.doctor import doctor
15
30
  from .commands.migrate import migrate
31
+ from .commands.ticks import ticks
16
32
 
17
33
 
18
34
  logger = setup_logger(__name__)
19
35
 
20
- _tick_streamer = None
36
+ _tick_engine = None
21
37
 
22
38
 
23
39
  def start_tick_capture():
24
40
  """
25
41
  Inicia captura contínua de ticks em background.
26
42
 
27
- O símbolo deve ser definido via variável:
43
+ Se a variável de ambiente MTCLI_SYMBOL estiver definida,
44
+ o mtcli iniciará automaticamente um TickEngine para
45
+ coletar ticks continuamente.
28
46
 
29
- MTCLI_SYMBOL=WINJ26
47
+ Isso permite manter um histórico próprio de ticks
48
+ independente do histórico do broker.
30
49
  """
31
50
 
32
- global _tick_streamer
51
+ global _tick_engine
33
52
 
34
- if _tick_streamer:
53
+ if _tick_engine:
35
54
  return
36
55
 
37
56
  symbol = os.getenv("MTCLI_SYMBOL")
38
57
 
39
58
  if not symbol:
40
- logger.info("Captura de ticks desativada (MTCLI_SYMBOL não definido).")
59
+ logger.info("Captura automática de ticks desativada (MTCLI_SYMBOL não definido).")
41
60
  return
42
61
 
43
62
  logger.info("Iniciando captura contínua de ticks para %s", symbol)
44
63
 
45
64
  try:
46
65
 
47
- _tick_streamer = TickStreamer(symbol)
48
-
49
- thread = threading.Thread(
50
- target=_tick_streamer.start,
51
- daemon=True,
52
- name="mtcli-tick-streamer",
53
- )
54
-
55
- thread.start()
66
+ _tick_engine = TickEngine([symbol])
67
+ _tick_engine.start()
56
68
 
57
69
  logger.info("Captura de ticks iniciada em background.")
58
70
 
59
71
  except Exception:
60
-
61
72
  logger.exception("Falha ao iniciar captura de ticks")
62
73
 
63
74
 
@@ -79,6 +90,7 @@ mt.add_command(doctor, name="doctor")
79
90
  mt.add_command(bars, name="bars")
80
91
  mt.add_command(doctor, name="dr")
81
92
  mt.add_command(migrate)
93
+ mt.add_command(ticks)
82
94
 
83
95
  loaded_plugins = load_plugins(mt)
84
96
 
@@ -88,7 +100,7 @@ logger.info("Plugins carregados: %s", loaded_plugins)
88
100
  @mt.command(name="plugins")
89
101
  def list_plugins():
90
102
  """
91
- Lista os plugins carregados.
103
+ Lista os plugins carregados no mtcli.
92
104
  """
93
105
 
94
106
  if not loaded_plugins:
@@ -0,0 +1,47 @@
1
+ """
2
+ Comando CLI para captura contínua de ticks.
3
+
4
+ Permite iniciar um TickEngine manualmente
5
+ para um ou mais símbolos.
6
+
7
+ Exemplo:
8
+
9
+ mt ticks WIN$N
10
+ mt ticks WIN$N WDO$N PETR4
11
+
12
+ A captura continua até o usuário interromper
13
+ com Ctrl+C.
14
+ """
15
+
16
+ import time
17
+ import click
18
+
19
+ from mtcli.marketdata.tick_engine import TickEngine
20
+
21
+
22
+ @click.command()
23
+ @click.argument("symbols", nargs=-1)
24
+ def ticks(symbols):
25
+ """
26
+ Inicia captura contínua de ticks para os símbolos informados.
27
+ """
28
+
29
+ if not symbols:
30
+ click.echo("Informe ao menos um símbolo.")
31
+ return
32
+
33
+ engine = TickEngine(symbols)
34
+
35
+ click.echo("Iniciando captura de ticks...")
36
+
37
+ engine.start()
38
+
39
+ try:
40
+
41
+ while True:
42
+ time.sleep(1)
43
+
44
+ except KeyboardInterrupt:
45
+
46
+ click.echo("\nEncerrando captura de ticks...")
47
+ engine.stop()
@@ -15,28 +15,23 @@ from pathlib import Path
15
15
  from datetime import datetime
16
16
  from .conf import DB_NAME
17
17
 
18
+
18
19
  DB_PATH = Path.home() / ".mtcli" / DB_NAME
19
20
  BACKUP_DIR = Path.home() / ".mtcli" / "backups"
20
21
 
22
+ _connection = None
23
+
21
24
 
22
25
  def get_connection():
23
26
  """
24
- Cria ou retorna uma conexão SQLite otimizada para ingestão
25
- contínua de ticks de mercado.
26
-
27
- Configurações aplicadas:
27
+ Retorna conexão singleton SQLite otimizada para ingestão
28
+ contínua de ticks.
29
+ """
28
30
 
29
- - WAL (Write Ahead Logging)
30
- - synchronous=NORMAL
31
- - temp_store em memória
32
- - mmap para leitura rápida
33
- - cache expandido
31
+ global _connection
34
32
 
35
- Returns
36
- -------
37
- sqlite3.Connection
38
- Conexão ativa com o banco SQLite.
39
- """
33
+ if _connection:
34
+ return _connection
40
35
 
41
36
  DB_PATH.parent.mkdir(parents=True, exist_ok=True)
42
37
  BACKUP_DIR.mkdir(parents=True, exist_ok=True)
@@ -50,6 +45,9 @@ def get_connection():
50
45
  conn.execute("PRAGMA cache_size=-200000")
51
46
  conn.execute("PRAGMA journal_size_limit=67108864")
52
47
 
48
+ # checkpoint automático
49
+ conn.execute("PRAGMA wal_autocheckpoint=1000")
50
+
53
51
  conn.execute("""
54
52
  CREATE TABLE IF NOT EXISTS schema_migrations(
55
53
  version INTEGER PRIMARY KEY,
@@ -59,22 +57,9 @@ def get_connection():
59
57
 
60
58
  conn.commit()
61
59
 
62
- return conn
63
-
60
+ _connection = conn
64
61
 
65
- # ==========================================================
66
- # CHECKPOINT
67
- # ==========================================================
68
-
69
- def wal_checkpoint(conn):
70
- """
71
- Executa checkpoint do WAL.
72
-
73
- Move os dados do arquivo `.wal` para o banco principal
74
- e reduz seu tamanho.
75
- """
76
-
77
- conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
62
+ return conn
78
63
 
79
64
 
80
65
  # ==========================================================
@@ -84,9 +69,6 @@ def wal_checkpoint(conn):
84
69
  def backup_database(conn):
85
70
  """
86
71
  Realiza backup diário seguro do banco SQLite.
87
-
88
- O backup utiliza a API nativa do SQLite,
89
- permitindo cópia consistente mesmo com o banco em uso.
90
72
  """
91
73
 
92
74
  now = datetime.now().strftime("%Y%m%d")
@@ -0,0 +1,134 @@
1
+ """
2
+ TickEngine - motor de captura contínua de ticks.
3
+
4
+ Responsável por:
5
+
6
+ - sincronizar histórico inicial
7
+ - capturar ticks em tempo real
8
+ - persistir ticks via TickRepository
9
+ """
10
+
11
+ import time
12
+ import threading
13
+ import MetaTrader5 as mt5
14
+
15
+ from datetime import datetime
16
+
17
+ from mtcli.mt5_context import mt5_conexao
18
+ from .tick_repository import TickRepository
19
+
20
+
21
+ class TickEngine:
22
+
23
+ POLL_INTERVAL = 0.2
24
+ BATCH_SIZE = 1000
25
+ OVERLAP_MS = 5
26
+
27
+ def __init__(self, symbols):
28
+
29
+ self.symbols = symbols
30
+
31
+ self.repositories = {
32
+ symbol: TickRepository()
33
+ for symbol in symbols
34
+ }
35
+
36
+ self.running = False
37
+ self.thread = None
38
+
39
+ def start(self):
40
+
41
+ if self.running:
42
+ return
43
+
44
+ self.running = True
45
+
46
+ self.thread = threading.Thread(
47
+ target=self._run,
48
+ daemon=True,
49
+ name="mtcli-tick-engine",
50
+ )
51
+
52
+ self.thread.start()
53
+
54
+ def stop(self):
55
+
56
+ self.running = False
57
+
58
+ if self.thread:
59
+ self.thread.join()
60
+
61
+ def _run(self):
62
+
63
+ with mt5_conexao():
64
+
65
+ last_positions = {}
66
+
67
+ for symbol in self.symbols:
68
+
69
+ repo = self.repositories[symbol]
70
+
71
+ # sincroniza histórico
72
+ repo.sync(symbol)
73
+
74
+ last_msc = repo._get_last_tick_msc(symbol)
75
+
76
+ if last_msc:
77
+ last_positions[symbol] = last_msc
78
+ else:
79
+ last_positions[symbol] = int(time.time() * 1000)
80
+
81
+ while self.running:
82
+
83
+ for symbol in self.symbols:
84
+ self._drain_symbol(symbol, last_positions)
85
+
86
+ time.sleep(self.POLL_INTERVAL)
87
+
88
+ def _drain_symbol(self, symbol, last_positions):
89
+
90
+ repo = self.repositories[symbol]
91
+
92
+ last_msc = last_positions[symbol]
93
+
94
+ start_dt = datetime.fromtimestamp(
95
+ (last_msc - self.OVERLAP_MS) / 1000
96
+ )
97
+
98
+ while True:
99
+
100
+ ticks = mt5.copy_ticks_from(
101
+ symbol,
102
+ start_dt,
103
+ self.BATCH_SIZE,
104
+ mt5.COPY_TICKS_ALL,
105
+ )
106
+
107
+ if ticks is None or len(ticks) == 0:
108
+ break
109
+
110
+ repo.conn.execute("BEGIN")
111
+
112
+ try:
113
+
114
+ repo._insert_ticks(symbol, ticks)
115
+
116
+ repo.cache.add_many(ticks)
117
+
118
+ repo.conn.commit()
119
+
120
+ except Exception:
121
+
122
+ repo.conn.rollback()
123
+ raise
124
+
125
+ last_msc = int(ticks[-1]["time_msc"])
126
+
127
+ last_positions[symbol] = last_msc + 1
128
+
129
+ start_dt = datetime.fromtimestamp(
130
+ (last_msc - self.OVERLAP_MS) / 1000
131
+ )
132
+
133
+ if len(ticks) < self.BATCH_SIZE:
134
+ break
@@ -3,91 +3,90 @@ TickRepository.
3
3
 
4
4
  Responsável por:
5
5
 
6
- - Persistir ticks no SQLite
7
- - Sincronizar histórico inicial
8
- - Consultas rápidas para engines (Renko etc)
6
+ - persistência de ticks
7
+ - sincronização histórica
8
+ - consultas rápidas
9
9
  """
10
10
 
11
11
  import MetaTrader5 as mt5
12
+
12
13
  from datetime import datetime, timedelta
13
14
 
14
- from ..database import get_connection, wal_checkpoint, backup_database
15
+ from ..database import get_connection, backup_database
15
16
  from .tick_cache import TickCache
16
17
  from mtcli.mt5_context import mt5_conexao
17
18
 
18
19
 
19
20
  class TickRepository:
20
21
 
21
- BATCH_SIZE = 200000
22
+ RANGE_WINDOW_MINUTES = 10
22
23
 
23
24
  def __init__(self):
24
25
 
25
26
  self.conn = get_connection()
26
27
  self.cache = TickCache()
27
28
 
28
- self.insert_counter = 0
29
29
  self.last_backup_day = None
30
30
 
31
31
  # ==========================================================
32
- # SINCRONIZAÇÃO HISTÓRICA
32
+ # SYNC HISTÓRICO
33
33
  # ==========================================================
34
34
 
35
35
  def sync(self, symbol: str, days_back: int = 1):
36
- """
37
- Sincroniza histórico de ticks a partir do broker.
38
- """
39
36
 
40
37
  total_inserted = 0
41
38
 
42
- with mt5_conexao():
39
+ end = datetime.now()
40
+
41
+ last_msc = self._get_last_tick_msc(symbol)
43
42
 
44
- last_msc = self._get_last_tick_msc(symbol)
43
+ if last_msc:
44
+ start = datetime.fromtimestamp((last_msc + 1) / 1000)
45
+ else:
46
+ start = end - timedelta(days=days_back)
45
47
 
46
- if last_msc:
47
- start = datetime.fromtimestamp((last_msc + 1) / 1000)
48
- else:
49
- start = datetime.now() - timedelta(days=days_back)
48
+ window = timedelta(minutes=self.RANGE_WINDOW_MINUTES)
50
49
 
51
- self.conn.execute("BEGIN")
50
+ with mt5_conexao():
51
+
52
+ while start < end:
52
53
 
53
- try:
54
+ chunk_end = min(start + window, end)
54
55
 
55
- while True:
56
+ ticks = mt5.copy_ticks_range(
57
+ symbol,
58
+ start,
59
+ chunk_end,
60
+ mt5.COPY_TICKS_ALL
61
+ )
56
62
 
57
- ticks = mt5.copy_ticks_from(
58
- symbol,
59
- start,
60
- self.BATCH_SIZE,
61
- mt5.COPY_TICKS_ALL,
62
- )
63
+ if ticks is not None and len(ticks) > 0:
63
64
 
64
- if ticks is None or len(ticks) == 0:
65
- break
65
+ self.conn.execute("BEGIN")
66
66
 
67
- inserted = self._insert_ticks(symbol, ticks)
67
+ try:
68
68
 
69
- total_inserted += inserted
69
+ inserted = self._insert_ticks(symbol, ticks)
70
70
 
71
- self.cache.add_many(ticks)
71
+ total_inserted += inserted
72
72
 
73
- last_msc = int(ticks[-1]["time_msc"])
73
+ self.cache.add_many(ticks)
74
74
 
75
- start = datetime.fromtimestamp((last_msc + 1) / 1000)
75
+ self.conn.commit()
76
76
 
77
- if len(ticks) < self.BATCH_SIZE:
78
- break
77
+ except Exception:
79
78
 
80
- self.conn.commit()
79
+ self.conn.rollback()
80
+ raise
81
81
 
82
- except Exception:
82
+ start = chunk_end
83
83
 
84
- self.conn.rollback()
85
- raise
84
+ self._daily_backup()
86
85
 
87
86
  return total_inserted
88
87
 
89
88
  # ==========================================================
90
- # INSERÇÃO
89
+ # INSERT
91
90
  # ==========================================================
92
91
 
93
92
  def _insert_ticks(self, symbol, ticks):
@@ -114,28 +113,12 @@ class TickRepository:
114
113
  symbol,time,time_msc,bid,ask,last,volume,flags
115
114
  )
116
115
  VALUES (?,?,?,?,?,?,?,?)
117
- ON CONFLICT(symbol,time) DO NOTHING
116
+ ON CONFLICT(symbol,time_msc) DO NOTHING
118
117
  """,
119
118
  data,
120
119
  )
121
120
 
122
- inserted = len(data)
123
-
124
- self.insert_counter += inserted
125
-
126
- if self.insert_counter >= 200000:
127
-
128
- wal_checkpoint(self.conn)
129
- self.insert_counter = 0
130
-
131
- today = datetime.now().date()
132
-
133
- if self.last_backup_day != today:
134
-
135
- backup_database(self.conn)
136
- self.last_backup_day = today
137
-
138
- return inserted
121
+ return len(data)
139
122
 
140
123
  # ==========================================================
141
124
  # CONSULTAS
@@ -180,7 +163,7 @@ class TickRepository:
180
163
  return cursor.fetchall()
181
164
 
182
165
  # ==========================================================
183
- # UTILITÁRIOS
166
+ # UTIL
184
167
  # ==========================================================
185
168
 
186
169
  def _get_last_tick_msc(self, symbol):
@@ -196,6 +179,15 @@ class TickRepository:
196
179
  (symbol,),
197
180
  )
198
181
 
199
- result = cursor.fetchone()
182
+ row = cursor.fetchone()
183
+
184
+ return row[0] if row and row[0] else None
185
+
186
+ def _daily_backup(self):
200
187
 
201
- return result[0] if result and result[0] else None
188
+ today = datetime.now().date()
189
+
190
+ if self.last_backup_day != today:
191
+
192
+ backup_database(self.conn)
193
+ self.last_backup_day = today
@@ -0,0 +1,56 @@
1
+ def upgrade(conn):
2
+
3
+ cursor = conn.execute("""
4
+ SELECT sql
5
+ FROM sqlite_master
6
+ WHERE type='table'
7
+ AND name='ticks'
8
+ """)
9
+
10
+ row = cursor.fetchone()
11
+
12
+ if not row:
13
+ return
14
+
15
+ if "PRIMARY KEY(symbol, time_msc)" in row[0]:
16
+ return
17
+
18
+ print("Rebuilding ticks table with PK(symbol,time_msc)...")
19
+
20
+ conn.execute("PRAGMA foreign_keys=OFF")
21
+
22
+ conn.execute("""
23
+ CREATE TABLE ticks_new(
24
+ symbol TEXT NOT NULL,
25
+ time INTEGER NOT NULL,
26
+ time_msc INTEGER NOT NULL,
27
+ bid REAL,
28
+ ask REAL,
29
+ last REAL,
30
+ volume REAL,
31
+ flags INTEGER,
32
+ PRIMARY KEY(symbol, time_msc)
33
+ ) WITHOUT ROWID
34
+ """)
35
+
36
+ conn.execute("""
37
+ INSERT INTO ticks_new
38
+ SELECT
39
+ symbol,
40
+ time,
41
+ COALESCE(time_msc,time),
42
+ bid,
43
+ ask,
44
+ last,
45
+ volume,
46
+ flags
47
+ FROM ticks
48
+ """)
49
+
50
+ conn.execute("DROP TABLE ticks")
51
+
52
+ conn.execute("ALTER TABLE ticks_new RENAME TO ticks")
53
+
54
+ conn.commit()
55
+
56
+ print("ticks table rebuilt with PK(symbol,time_msc)")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mtcli"
3
- version = "3.8.0.dev3"
3
+ version = "3.8.0.dev5"
4
4
  description = "Aplicativo CLI para exibir gráficos do MetaTrader 5 screen reader friendly"
5
5
  authors = [
6
6
  {name = "Valmir França",email = "vfranca3@gmail.com"}
@@ -1,93 +0,0 @@
1
- """
2
- Motor de captura de ticks multi-ativo.
3
- """
4
-
5
- import time
6
- import threading
7
- import MetaTrader5 as mt5
8
- from datetime import datetime
9
-
10
- from mtcli.mt5_context import mt5_conexao
11
- from .tick_repository import TickRepository
12
-
13
-
14
- class TickEngine:
15
-
16
- POLL_INTERVAL = 0.2
17
-
18
- def __init__(self, symbols):
19
-
20
- self.symbols = symbols
21
-
22
- self.repositories = {
23
- symbol: TickRepository()
24
- for symbol in symbols
25
- }
26
-
27
- self.running = False
28
-
29
- def start(self):
30
-
31
- thread = threading.Thread(
32
- target=self._run,
33
- daemon=True
34
- )
35
-
36
- self.running = True
37
- thread.start()
38
-
39
- def stop(self):
40
-
41
- self.running = False
42
-
43
- def _run(self):
44
-
45
- with mt5_conexao():
46
-
47
- last_positions = {}
48
-
49
- for symbol in self.symbols:
50
-
51
- repo = self.repositories[symbol]
52
- last_msc = repo._get_last_tick_msc(symbol)
53
-
54
- if last_msc:
55
- last_positions[symbol] = last_msc + 1
56
- else:
57
- last_positions[symbol] = int(time.time() * 1000)
58
-
59
- while self.running:
60
-
61
- for symbol in self.symbols:
62
-
63
- repo = self.repositories[symbol]
64
-
65
- start = last_positions[symbol]
66
-
67
- ticks = mt5.copy_ticks_from(
68
- symbol,
69
- datetime.fromtimestamp(start / 1000),
70
- 1000,
71
- mt5.COPY_TICKS_ALL
72
- )
73
-
74
- if ticks is None or len(ticks) == 0:
75
- continue
76
-
77
- repo.conn.execute("BEGIN")
78
-
79
- try:
80
-
81
- repo._insert_ticks(symbol, ticks)
82
-
83
- repo.cache.add_many(ticks)
84
-
85
- repo.conn.commit()
86
-
87
- except Exception:
88
-
89
- repo.conn.rollback()
90
-
91
- last_positions[symbol] = int(ticks[-1]["time_msc"]) + 1
92
-
93
- time.sleep(self.POLL_INTERVAL)
@@ -1,75 +0,0 @@
1
- """
2
- Captura contínua de ticks.
3
-
4
- Objetivo:
5
-
6
- - eliminar dependência do histórico do broker
7
- - manter histórico próprio
8
- """
9
-
10
- import time
11
- import MetaTrader5 as mt5
12
- from datetime import datetime
13
-
14
- from mtcli.mt5_context import mt5_conexao
15
- from .tick_repository import TickRepository
16
-
17
-
18
- class TickStreamer:
19
-
20
- def __init__(self, symbol):
21
-
22
- self.symbol = symbol
23
- self.repo = TickRepository()
24
-
25
- self.running = False
26
-
27
- def start(self):
28
- """
29
- Inicia captura contínua de ticks.
30
- """
31
-
32
- self.running = True
33
-
34
- with mt5_conexao():
35
-
36
- last_msc = self.repo._get_last_tick_msc(self.symbol)
37
-
38
- if last_msc:
39
- start = last_msc + 1
40
- else:
41
- start = int(time.time() * 1000)
42
-
43
- while self.running:
44
-
45
- ticks = mt5.copy_ticks_from(
46
- self.symbol,
47
- datetime.fromtimestamp(start / 1000),
48
- 1000,
49
- mt5.COPY_TICKS_ALL,
50
- )
51
-
52
- if ticks is None or len(ticks) == 0:
53
-
54
- time.sleep(0.1)
55
- continue
56
-
57
- self.repo.conn.execute("BEGIN")
58
-
59
- try:
60
-
61
- self.repo._insert_ticks(self.symbol, ticks)
62
-
63
- self.repo.cache.add_many(ticks)
64
-
65
- self.repo.conn.commit()
66
-
67
- except Exception:
68
-
69
- self.repo.conn.rollback()
70
-
71
- start = int(ticks[-1]["time_msc"]) + 1
72
-
73
- def stop(self):
74
-
75
- self.running = False
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes