mtcli 3.5.0.dev3__tar.gz → 3.6.0.dev0__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 (76) hide show
  1. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/PKG-INFO +1 -1
  2. mtcli-3.6.0.dev0/mtcli/cli.py +32 -0
  3. mtcli-3.6.0.dev0/mtcli/conf.py +199 -0
  4. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/data/csv.py +1 -1
  5. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/database.py +54 -54
  6. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/marketdata/tick_cache.py +24 -24
  7. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/marketdata/tick_repository.py +142 -142
  8. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/models/bar_model.py +105 -105
  9. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/models/consecutive_bars_model.py +77 -77
  10. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/models/unconsecutive_bar_model.py +67 -67
  11. mtcli-3.6.0.dev0/mtcli/plugin.py +18 -0
  12. mtcli-3.6.0.dev0/mtcli/plugin_loader.py +57 -0
  13. mtcli-3.6.0.dev0/mtcli/plugins/hello.py +20 -0
  14. mtcli-3.6.0.dev0/mtcli/plugins/media_movel/__init__.py +5 -0
  15. mtcli-3.5.0.dev3/mtcli/plugins/media_movel/command.py → mtcli-3.6.0.dev0/mtcli/plugins/media_movel/cli.py +98 -99
  16. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/plugins/media_movel/tests/test_mm.py +13 -13
  17. mtcli-3.6.0.dev0/mtcli/plugins/range_medio/__init__.py +5 -0
  18. mtcli-3.5.0.dev3/mtcli/plugins/range_medio/command.py → mtcli-3.6.0.dev0/mtcli/plugins/range_medio/cli.py +32 -33
  19. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/plugins/range_medio/models/average_range_model.py +29 -29
  20. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/plugins/range_medio/tests/test_rm.py +1 -1
  21. mtcli-3.6.0.dev0/mtcli/plugins/volume_medio/__init__.py +5 -0
  22. mtcli-3.5.0.dev3/mtcli/plugins/volume_medio/command.py → mtcli-3.6.0.dev0/mtcli/plugins/volume_medio/cli.py +41 -42
  23. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/plugins/volume_medio/models/model_average_volume.py +31 -31
  24. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/plugins/volume_medio/tests/test_vm.py +1 -1
  25. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/views/close_view.py +37 -37
  26. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/views/full_view.py +65 -65
  27. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/views/high_view.py +37 -37
  28. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/views/low_view.py +37 -37
  29. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/views/min_view.py +42 -42
  30. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/views/open_view.py +37 -37
  31. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/views/ranges_view.py +41 -41
  32. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/views/rates_view.py +41 -41
  33. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/pyproject.toml +3 -5
  34. mtcli-3.5.0.dev3/mtcli/commands/conf.py +0 -50
  35. mtcli-3.5.0.dev3/mtcli/commands/logs.py +0 -42
  36. mtcli-3.5.0.dev3/mtcli/conf.py +0 -102
  37. mtcli-3.5.0.dev3/mtcli/mt.py +0 -45
  38. mtcli-3.5.0.dev3/mtcli/plugin.py +0 -9
  39. mtcli-3.5.0.dev3/mtcli/plugins/media_movel/__init__.py +0 -3
  40. mtcli-3.5.0.dev3/mtcli/plugins/range_medio/__init__.py +0 -2
  41. mtcli-3.5.0.dev3/mtcli/plugins/volume_medio/__init__.py +0 -3
  42. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/LICENSE +0 -0
  43. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/README.md +0 -0
  44. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/__init__.py +0 -0
  45. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/commands/__init__.py +0 -0
  46. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/commands/bars.py +0 -0
  47. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/conecta.py +0 -0
  48. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/data/__init__.py +0 -0
  49. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/data/base.py +0 -0
  50. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/data/mt5.py +0 -0
  51. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/domain/__init__.py +0 -0
  52. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/domain/timeframe.py +0 -0
  53. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/logger.py +0 -0
  54. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/marketdata/__init__.py +0 -0
  55. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/models/__init__.py +0 -0
  56. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/models/bars_model.py +0 -0
  57. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/models/chart_model.py +0 -0
  58. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/models/conf_model.py +0 -0
  59. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/models/rates_model.py +0 -0
  60. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/models/signals_model.py +0 -0
  61. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/mt5_context.py +0 -0
  62. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/plugins/__init__.py +0 -0
  63. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/plugins/media_movel/conf.py +0 -0
  64. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/plugins/media_movel/models/__init__.py +0 -0
  65. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/plugins/media_movel/models/model_media_movel.py +0 -0
  66. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/plugins/media_movel/tests/__init__.py +0 -0
  67. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/plugins/media_movel/tests/test_model_media_movel.py +0 -0
  68. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/plugins/range_medio/conf.py +0 -0
  69. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/plugins/range_medio/models/__init__.py +0 -0
  70. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/plugins/range_medio/tests/__init__.py +0 -0
  71. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/plugins/volume_medio/conf.py +0 -0
  72. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/plugins/volume_medio/models/__init__.py +0 -0
  73. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/plugins/volume_medio/tests/__init__.py +0 -0
  74. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/views/__init__.py +0 -0
  75. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/views/vars_view.py +0 -0
  76. {mtcli-3.5.0.dev3 → mtcli-3.6.0.dev0}/mtcli/views/volumes_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mtcli
3
- Version: 3.5.0.dev3
3
+ Version: 3.6.0.dev0
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
@@ -0,0 +1,32 @@
1
+ """
2
+ Comando principal do mtcli.
3
+ """
4
+
5
+ import click
6
+ from mtcli.plugin_loader import load_plugins
7
+
8
+ # --- CLI principal ---
9
+ @click.group(context_settings={"max_content_width": 120})
10
+ @click.version_option(package_name="mtcli")
11
+ def mt():
12
+ """
13
+ MTCLI - CLI para gráficos candlestick screen reader friendly.
14
+
15
+ Comandos disponíveis:
16
+ - Subcomandos (bars, conf, logs)
17
+ - Plugins internos e externos
18
+ """
19
+ pass
20
+
21
+ # --- Carrega subcomandos do mt e plugins ---
22
+ # Os comandos do diretório commands são registrados aqui diretamente
23
+ from mtcli.commands import bars
24
+
25
+ mt.add_command(bars.bars, name="bars")
26
+
27
+ # --- Carrega plugins internos e externos via plugin_loader ---
28
+ load_plugins(mt)
29
+
30
+ # --- Entry point para execução direta ---
31
+ if __name__ == "__main__":
32
+ mt()
@@ -0,0 +1,199 @@
1
+ """
2
+ Configurações principais do mtcli.
3
+
4
+ Este módulo centraliza a leitura de configurações provenientes de:
5
+
6
+ 1. Variáveis de ambiente
7
+ 2. Arquivo mtcli.ini
8
+
9
+ As variáveis de ambiente sempre têm prioridade sobre o arquivo INI.
10
+
11
+ Também fornece utilidades para obter o caminho de arquivos do
12
+ terminal MetaTrader5 e selecionar a fonte de dados (CSV ou MT5).
13
+
14
+ O acesso ao terminal MT5 é realizado apenas sob demanda para evitar
15
+ efeitos colaterais durante a importação do módulo (import side effects),
16
+ facilitando testes automatizados e execução em ambientes CI/CD.
17
+ """
18
+
19
+ import os
20
+ import configparser
21
+
22
+ import MetaTrader5 as mt5
23
+
24
+ from mtcli.mt5_context import mt5_conexao
25
+
26
+ # ---------------------------------------------------------
27
+ # Carregamento do arquivo de configuração
28
+ # ---------------------------------------------------------
29
+
30
+ CONFIG_FILE = "mtcli.ini"
31
+ SECTION = "DEFAULT"
32
+
33
+ config = configparser.ConfigParser()
34
+ config.read(CONFIG_FILE)
35
+
36
+
37
+ def get_config_value(key: str, cast=None, fallback=None):
38
+ """
39
+ Retorna um valor de configuração.
40
+
41
+ A prioridade de leitura é:
42
+ 1. Variável de ambiente
43
+ 2. Arquivo mtcli.ini
44
+ 3. Valor fallback
45
+
46
+ Args:
47
+ key (str): Nome da chave de configuração.
48
+ cast (type | None): Tipo de conversão (ex: int, float).
49
+ fallback (Any): Valor padrão caso não encontrado.
50
+
51
+ Returns:
52
+ Any: valor convertido ou fallback.
53
+ """
54
+ value = os.getenv(key)
55
+
56
+ if value is None:
57
+ try:
58
+ if cast == int:
59
+ value = config.getint(SECTION, key, fallback=fallback)
60
+ elif cast == float:
61
+ value = config.getfloat(SECTION, key, fallback=fallback)
62
+ else:
63
+ value = config.get(SECTION, key, fallback=fallback)
64
+ except (configparser.NoOptionError, ValueError):
65
+ value = fallback
66
+ else:
67
+ if cast:
68
+ try:
69
+ value = cast(value)
70
+ except ValueError:
71
+ value = fallback
72
+
73
+ return value
74
+
75
+
76
+ # ---------------------------------------------------------
77
+ # Configurações gerais
78
+ # ---------------------------------------------------------
79
+
80
+ SYMBOL = get_config_value("symbol", fallback="WIN$N")
81
+ DIGITOS = get_config_value("digitos", cast=int, fallback=2)
82
+ PERIOD = get_config_value("period", fallback="D1")
83
+ BARS = get_config_value("count", cast=int, fallback=999)
84
+
85
+ VIEW = get_config_value("view", fallback="ch")
86
+ VOLUME = get_config_value("volume", fallback="tick")
87
+ DATE = get_config_value("date", fallback="")
88
+
89
+ # ---------------------------------------------------------
90
+ # Configurações de leitura de candles
91
+ # ---------------------------------------------------------
92
+
93
+ LATERAL = get_config_value("lateral", fallback="doji")
94
+ ALTA = get_config_value("alta", fallback="verde")
95
+ BAIXA = get_config_value("baixa", fallback="vermelho")
96
+
97
+ ROMPIMENTO_ALTA = get_config_value("rompimento_alta", fallback="c")
98
+ ROMPIMENTO_BAIXA = get_config_value("rompimento_baixa", fallback="v")
99
+
100
+ PERCENTUAL_ROMPIMENTO = get_config_value(
101
+ "percentual_rompimento", cast=int, fallback=50
102
+ )
103
+
104
+ PERCENTUAL_DOJI = get_config_value(
105
+ "percentual_doji", cast=int, fallback=10
106
+ )
107
+
108
+ # ---------------------------------------------------------
109
+ # Configurações de padrões de barra
110
+ # ---------------------------------------------------------
111
+
112
+ UP_BAR = get_config_value("up_bar", fallback="asc")
113
+ DOWN_BAR = get_config_value("down_bar", fallback="desc")
114
+
115
+ INSIDE_BAR = get_config_value("inside_bar", fallback="ib")
116
+ OUTSIDE_BAR = get_config_value("outside_bar", fallback="ob")
117
+
118
+ SOMBRA_SUPERIOR = get_config_value("sombra_superior", fallback="top")
119
+ SOMBRA_INFERIOR = get_config_value("sombra_inferior", fallback="bottom")
120
+
121
+ # ---------------------------------------------------------
122
+ # Fonte de dados
123
+ # ---------------------------------------------------------
124
+
125
+ DATA_SOURCE = get_config_value("dados", fallback="mt5").lower()
126
+
127
+ # caminho inicial (pode vir do ini/env)
128
+ _INITIAL_CSV_PATH = get_config_value("mt5_pasta", fallback="")
129
+
130
+
131
+ def get_csv_path():
132
+ """
133
+ Retorna o caminho da pasta de arquivos do MT5 ou CSV.
134
+
135
+ Se o caminho não estiver definido no mtcli.ini ou nas
136
+ variáveis de ambiente, o terminal MT5 é consultado para
137
+ descobrir automaticamente a pasta MQL5/Files.
138
+
139
+ Returns:
140
+ str: caminho normalizado da pasta de arquivos.
141
+ """
142
+ if _INITIAL_CSV_PATH:
143
+ path = _INITIAL_CSV_PATH
144
+ else:
145
+ with mt5_conexao():
146
+ terminal_info = mt5.terminal_info()
147
+
148
+ if terminal_info is None:
149
+ raise RuntimeError(
150
+ "Não foi possível obter as informações do terminal MT5."
151
+ )
152
+
153
+ path = os.path.join(terminal_info.data_path, "MQL5", "Files")
154
+
155
+ return os.path.normpath(path) + os.sep
156
+
157
+
158
+ # ---------------------------------------------------------
159
+ # Factory de DataSource
160
+ # ---------------------------------------------------------
161
+
162
+ def get_data_source(source=None):
163
+ """
164
+ Retorna a fonte de dados configurada.
165
+
166
+ Args:
167
+ source (str | None): sobrescreve DATA_SOURCE se fornecido.
168
+
169
+ Returns:
170
+ CsvDataSource | MT5DataSource
171
+
172
+ Raises:
173
+ ValueError: se a fonte de dados não for reconhecida.
174
+ """
175
+ from mtcli.data import CsvDataSource, MT5DataSource
176
+
177
+ src = source.lower() if source else DATA_SOURCE
178
+
179
+ if src == "csv":
180
+ return CsvDataSource()
181
+
182
+ if src == "mt5":
183
+ return MT5DataSource()
184
+
185
+ raise ValueError(f"Fonte de dados desconhecida: {src}")
186
+
187
+
188
+ # ---------------------------------------------------------
189
+ # Timeframes suportados
190
+ # ---------------------------------------------------------
191
+
192
+ _HOURS = [12, 8, 6, 4, 3, 2, 1]
193
+ _MINUTES = [30, 20, 15, 12, 10, 6, 5, 4, 3, 2, 1]
194
+
195
+ TIMEFRAMES = (
196
+ ["mn1", "w1", "d1"]
197
+ + [f"h{i}" for i in _HOURS]
198
+ + [f"m{i}" for i in _MINUTES]
199
+ )
@@ -16,7 +16,7 @@ class CsvDataSource(DataSourceBase):
16
16
 
17
17
  def get_data(self, symbol, period, count=100):
18
18
  """Retorna dados CSV em uma lista de lista."""
19
- file_path = os.path.join(conf.csv_path, f"{symbol}{period}.csv")
19
+ file_path = os.path.join(conf._INITIAL_CSV_PATH, f"{symbol}{period}.csv")
20
20
  logger.info(f"Iniciando coleta de dados via CSV: {file_path}.")
21
21
  csv_data = []
22
22
  try:
@@ -1,54 +1,54 @@
1
- """
2
- Database core para mtcli.
3
-
4
- Responsável por:
5
- - Criar conexão SQLite
6
- - Ativar WAL
7
- - Garantir schema
8
- """
9
-
10
- import sqlite3
11
- from pathlib import Path
12
-
13
-
14
- DB_PATH = Path.home() / ".mtcli" / "marketdata.db"
15
-
16
-
17
- def get_connection():
18
- """
19
- Retorna conexão SQLite configurada.
20
- """
21
- DB_PATH.parent.mkdir(parents=True, exist_ok=True)
22
-
23
- conn = sqlite3.connect(DB_PATH)
24
- conn.execute("PRAGMA journal_mode=WAL;")
25
- conn.execute("PRAGMA synchronous=NORMAL;")
26
-
27
- _ensure_schema(conn)
28
- return conn
29
-
30
-
31
- def _ensure_schema(conn):
32
- conn.execute(
33
- """
34
- CREATE TABLE IF NOT EXISTS ticks (
35
- symbol TEXT NOT NULL,
36
- time INTEGER NOT NULL,
37
- bid REAL,
38
- ask REAL,
39
- last REAL,
40
- volume REAL,
41
- flags INTEGER,
42
- PRIMARY KEY (symbol, time)
43
- );
44
- """
45
- )
46
-
47
- conn.execute(
48
- """
49
- CREATE INDEX IF NOT EXISTS idx_ticks_symbol_time
50
- ON ticks(symbol, time);
51
- """
52
- )
53
-
54
- conn.commit()
1
+ """
2
+ Database core para mtcli.
3
+
4
+ Responsável por:
5
+ - Criar conexão SQLite
6
+ - Ativar WAL
7
+ - Garantir schema
8
+ """
9
+
10
+ import sqlite3
11
+ from pathlib import Path
12
+
13
+
14
+ DB_PATH = Path.home() / ".mtcli" / "marketdata.db"
15
+
16
+
17
+ def get_connection():
18
+ """
19
+ Retorna conexão SQLite configurada.
20
+ """
21
+ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
22
+
23
+ conn = sqlite3.connect(DB_PATH)
24
+ conn.execute("PRAGMA journal_mode=WAL;")
25
+ conn.execute("PRAGMA synchronous=NORMAL;")
26
+
27
+ _ensure_schema(conn)
28
+ return conn
29
+
30
+
31
+ def _ensure_schema(conn):
32
+ conn.execute(
33
+ """
34
+ CREATE TABLE IF NOT EXISTS ticks (
35
+ symbol TEXT NOT NULL,
36
+ time INTEGER NOT NULL,
37
+ bid REAL,
38
+ ask REAL,
39
+ last REAL,
40
+ volume REAL,
41
+ flags INTEGER,
42
+ PRIMARY KEY (symbol, time)
43
+ );
44
+ """
45
+ )
46
+
47
+ conn.execute(
48
+ """
49
+ CREATE INDEX IF NOT EXISTS idx_ticks_symbol_time
50
+ ON ticks(symbol, time);
51
+ """
52
+ )
53
+
54
+ conn.commit()
@@ -1,24 +1,24 @@
1
- """
2
- Cache de ticks em memória.
3
- """
4
-
5
- from collections import deque
6
-
7
-
8
- class TickCache:
9
- """
10
- Mantém janela recente de ticks em memória.
11
- """
12
-
13
- def __init__(self, max_size=10000):
14
- self.buffer = deque(maxlen=max_size)
15
-
16
- def add_many(self, ticks):
17
- for t in ticks:
18
- self.buffer.append(t)
19
-
20
- def get_all(self):
21
- return list(self.buffer)
22
-
23
- def clear(self):
24
- self.buffer.clear()
1
+ """
2
+ Cache de ticks em memória.
3
+ """
4
+
5
+ from collections import deque
6
+
7
+
8
+ class TickCache:
9
+ """
10
+ Mantém janela recente de ticks em memória.
11
+ """
12
+
13
+ def __init__(self, max_size=10000):
14
+ self.buffer = deque(maxlen=max_size)
15
+
16
+ def add_many(self, ticks):
17
+ for t in ticks:
18
+ self.buffer.append(t)
19
+
20
+ def get_all(self):
21
+ return list(self.buffer)
22
+
23
+ def clear(self):
24
+ self.buffer.clear()
@@ -1,142 +1,142 @@
1
- """
2
- TickRepository profissional com:
3
-
4
- - Paginação automática
5
- - Sincronização incremental robusta
6
- - Integração com mt5_conexao
7
- - Proteção contra loops infinitos
8
- """
9
-
10
- import MetaTrader5 as mt5
11
- from datetime import datetime, timedelta
12
-
13
- from ..database import get_connection
14
- from .tick_cache import TickCache
15
- from mtcli.mt5_context import mt5_conexao
16
-
17
-
18
- class TickRepository:
19
-
20
- BATCH_SIZE = 200000 # tamanho seguro por lote
21
-
22
- def __init__(self):
23
- self.conn = get_connection()
24
- self.cache = TickCache()
25
-
26
- # ============================================================
27
- # SINCRONIZAÇÃO COM PAGINAÇÃO
28
- # ============================================================
29
-
30
- def sync(self, symbol: str, days_back: int = 1):
31
- """
32
- Sincroniza banco com MT5 usando paginação.
33
-
34
- Busca todos os ticks disponíveis desde o último timestamp.
35
- """
36
-
37
- total_inserted = 0
38
-
39
- with mt5_conexao():
40
-
41
- last_time = self._get_last_tick_time(symbol)
42
-
43
- if last_time:
44
- start = datetime.fromtimestamp(last_time + 1)
45
- else:
46
- start = datetime.now() - timedelta(days=days_back)
47
-
48
- while True:
49
-
50
- ticks = mt5.copy_ticks_from(
51
- symbol,
52
- start,
53
- self.BATCH_SIZE,
54
- mt5.COPY_TICKS_ALL,
55
- )
56
-
57
- if ticks is None or len(ticks) == 0:
58
- break
59
-
60
- inserted = self._insert_ticks(symbol, ticks)
61
- total_inserted += inserted
62
-
63
- self.cache.add_many(ticks)
64
-
65
- # Atualiza início para próximo tick
66
- ultimo_timestamp = int(ticks[-1]["time"])
67
- novo_start = datetime.fromtimestamp(ultimo_timestamp + 1)
68
-
69
- # Proteção contra loop infinito
70
- if novo_start <= start:
71
- break
72
-
73
- start = novo_start
74
-
75
- # Se retornou menos que o batch, acabou histórico
76
- if len(ticks) < self.BATCH_SIZE:
77
- break
78
-
79
- return total_inserted
80
-
81
- # ============================================================
82
- # INSERÇÃO
83
- # ============================================================
84
-
85
- def _insert_ticks(self, symbol, ticks):
86
-
87
- cursor = self.conn.cursor()
88
-
89
- data = [
90
- (
91
- symbol,
92
- int(t["time"]),
93
- float(t["bid"]),
94
- float(t["ask"]),
95
- float(t["last"]),
96
- float(t["volume"]),
97
- int(t["flags"]),
98
- )
99
- for t in ticks
100
- ]
101
-
102
- cursor.executemany(
103
- """
104
- INSERT OR IGNORE INTO ticks
105
- VALUES (?, ?, ?, ?, ?, ?, ?)
106
- """,
107
- data,
108
- )
109
-
110
- self.conn.commit()
111
- return cursor.rowcount
112
-
113
- # ============================================================
114
- # CONSULTAS
115
- # ============================================================
116
-
117
- def get_ticks_between(self, symbol, start_ts, end_ts):
118
- cursor = self.conn.cursor()
119
- cursor.execute(
120
- """
121
- SELECT time, bid, ask, last, volume, flags
122
- FROM ticks
123
- WHERE symbol = ?
124
- AND time BETWEEN ? AND ?
125
- ORDER BY time ASC
126
- """,
127
- (symbol, start_ts, end_ts),
128
- )
129
- return cursor.fetchall()
130
-
131
- def _get_last_tick_time(self, symbol):
132
- cursor = self.conn.cursor()
133
- cursor.execute(
134
- """
135
- SELECT MAX(time)
136
- FROM ticks
137
- WHERE symbol = ?
138
- """,
139
- (symbol,),
140
- )
141
- result = cursor.fetchone()
142
- return result[0] if result and result[0] else None
1
+ """
2
+ TickRepository profissional com:
3
+
4
+ - Paginação automática
5
+ - Sincronização incremental robusta
6
+ - Integração com mt5_conexao
7
+ - Proteção contra loops infinitos
8
+ """
9
+
10
+ import MetaTrader5 as mt5
11
+ from datetime import datetime, timedelta
12
+
13
+ from ..database import get_connection
14
+ from .tick_cache import TickCache
15
+ from mtcli.mt5_context import mt5_conexao
16
+
17
+
18
+ class TickRepository:
19
+
20
+ BATCH_SIZE = 200000 # tamanho seguro por lote
21
+
22
+ def __init__(self):
23
+ self.conn = get_connection()
24
+ self.cache = TickCache()
25
+
26
+ # ============================================================
27
+ # SINCRONIZAÇÃO COM PAGINAÇÃO
28
+ # ============================================================
29
+
30
+ def sync(self, symbol: str, days_back: int = 1):
31
+ """
32
+ Sincroniza banco com MT5 usando paginação.
33
+
34
+ Busca todos os ticks disponíveis desde o último timestamp.
35
+ """
36
+
37
+ total_inserted = 0
38
+
39
+ with mt5_conexao():
40
+
41
+ last_time = self._get_last_tick_time(symbol)
42
+
43
+ if last_time:
44
+ start = datetime.fromtimestamp(last_time + 1)
45
+ else:
46
+ start = datetime.now() - timedelta(days=days_back)
47
+
48
+ while True:
49
+
50
+ ticks = mt5.copy_ticks_from(
51
+ symbol,
52
+ start,
53
+ self.BATCH_SIZE,
54
+ mt5.COPY_TICKS_ALL,
55
+ )
56
+
57
+ if ticks is None or len(ticks) == 0:
58
+ break
59
+
60
+ inserted = self._insert_ticks(symbol, ticks)
61
+ total_inserted += inserted
62
+
63
+ self.cache.add_many(ticks)
64
+
65
+ # Atualiza início para próximo tick
66
+ ultimo_timestamp = int(ticks[-1]["time"])
67
+ novo_start = datetime.fromtimestamp(ultimo_timestamp + 1)
68
+
69
+ # Proteção contra loop infinito
70
+ if novo_start <= start:
71
+ break
72
+
73
+ start = novo_start
74
+
75
+ # Se retornou menos que o batch, acabou histórico
76
+ if len(ticks) < self.BATCH_SIZE:
77
+ break
78
+
79
+ return total_inserted
80
+
81
+ # ============================================================
82
+ # INSERÇÃO
83
+ # ============================================================
84
+
85
+ def _insert_ticks(self, symbol, ticks):
86
+
87
+ cursor = self.conn.cursor()
88
+
89
+ data = [
90
+ (
91
+ symbol,
92
+ int(t["time"]),
93
+ float(t["bid"]),
94
+ float(t["ask"]),
95
+ float(t["last"]),
96
+ float(t["volume"]),
97
+ int(t["flags"]),
98
+ )
99
+ for t in ticks
100
+ ]
101
+
102
+ cursor.executemany(
103
+ """
104
+ INSERT OR IGNORE INTO ticks
105
+ VALUES (?, ?, ?, ?, ?, ?, ?)
106
+ """,
107
+ data,
108
+ )
109
+
110
+ self.conn.commit()
111
+ return cursor.rowcount
112
+
113
+ # ============================================================
114
+ # CONSULTAS
115
+ # ============================================================
116
+
117
+ def get_ticks_between(self, symbol, start_ts, end_ts):
118
+ cursor = self.conn.cursor()
119
+ cursor.execute(
120
+ """
121
+ SELECT time, bid, ask, last, volume, flags
122
+ FROM ticks
123
+ WHERE symbol = ?
124
+ AND time BETWEEN ? AND ?
125
+ ORDER BY time ASC
126
+ """,
127
+ (symbol, start_ts, end_ts),
128
+ )
129
+ return cursor.fetchall()
130
+
131
+ def _get_last_tick_time(self, symbol):
132
+ cursor = self.conn.cursor()
133
+ cursor.execute(
134
+ """
135
+ SELECT MAX(time)
136
+ FROM ticks
137
+ WHERE symbol = ?
138
+ """,
139
+ (symbol,),
140
+ )
141
+ result = cursor.fetchone()
142
+ return result[0] if result and result[0] else None