mtcli 3.8.0.dev0__py3-none-any.whl → 3.8.0.dev2__py3-none-any.whl

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.
mtcli/cli.py CHANGED
@@ -1,11 +1,5 @@
1
1
  """
2
2
  CLI principal do mtcli.
3
-
4
- Este módulo define o grupo principal `mt`
5
- e inicializa o carregamento de plugins.
6
-
7
- Também inicia a captura contínua de ticks para manter
8
- um histórico próprio independente do broker.
9
3
  """
10
4
 
11
5
  import os
@@ -23,11 +17,6 @@ from .commands.migrate import migrate
23
17
 
24
18
  logger = setup_logger(__name__)
25
19
 
26
-
27
- # ---------------------------------------------------------
28
- # Configuração de captura de ticks
29
- # ---------------------------------------------------------
30
-
31
20
  _tick_streamer = None
32
21
 
33
22
 
@@ -35,7 +24,7 @@ def start_tick_capture():
35
24
  """
36
25
  Inicia captura contínua de ticks em background.
37
26
 
38
- O símbolo pode ser configurado via variável de ambiente:
27
+ O símbolo deve ser definido via variável:
39
28
 
40
29
  MTCLI_SYMBOL=WINJ26
41
30
  """
@@ -45,7 +34,7 @@ def start_tick_capture():
45
34
  if _tick_streamer:
46
35
  return
47
36
 
48
- symbol = os.getenv("SYMBOL")
37
+ symbol = os.getenv("MTCLI_SYMBOL")
49
38
 
50
39
  if not symbol:
51
40
  logger.info("Captura de ticks desativada (MTCLI_SYMBOL não definido).")
@@ -53,22 +42,24 @@ def start_tick_capture():
53
42
 
54
43
  logger.info("Iniciando captura contínua de ticks para %s", symbol)
55
44
 
56
- _tick_streamer = TickStreamer(symbol)
45
+ try:
46
+
47
+ _tick_streamer = TickStreamer(symbol)
48
+
49
+ thread = threading.Thread(
50
+ target=_tick_streamer.start,
51
+ daemon=True,
52
+ name="mtcli-tick-streamer",
53
+ )
57
54
 
58
- thread = threading.Thread(
59
- target=_tick_streamer.start,
60
- daemon=True,
61
- name="mtcli-tick-streamer",
62
- )
55
+ thread.start()
63
56
 
64
- thread.start()
57
+ logger.info("Captura de ticks iniciada em background.")
65
58
 
66
- logger.info("Captura de ticks iniciada em background.")
59
+ except Exception:
67
60
 
61
+ logger.exception("Falha ao iniciar captura de ticks")
68
62
 
69
- # ---------------------------------------------------------
70
- # CLI principal
71
- # ---------------------------------------------------------
72
63
 
73
64
  @click.group(context_settings={"max_content_width": 120}, invoke_without_command=True)
74
65
  @click.version_option(package_name="mtcli")
@@ -76,45 +67,28 @@ def start_tick_capture():
76
67
  def mt(ctx):
77
68
  """
78
69
  CLI principal do mtcli.
79
-
80
- Exibe gráficos e informações de mercado
81
- em formato textual compatível com leitores de tela.
82
70
  """
83
71
 
84
- # inicia captura de ticks
85
72
  start_tick_capture()
86
73
 
87
74
  if ctx.invoked_subcommand is None:
88
75
  click.echo(ctx.get_help())
89
76
 
90
77
 
91
- # ---------------------------------------------------------
92
- # Comandos internos
93
- # ---------------------------------------------------------
94
-
95
78
  mt.add_command(doctor, name="doctor")
96
79
  mt.add_command(bars, name="bars")
97
80
  mt.add_command(doctor, name="dr")
98
81
  mt.add_command(migrate)
99
82
 
100
-
101
- # ---------------------------------------------------------
102
- # Carregamento de plugins
103
- # ---------------------------------------------------------
104
-
105
83
  loaded_plugins = load_plugins(mt)
106
84
 
107
85
  logger.info("Plugins carregados: %s", loaded_plugins)
108
86
 
109
87
 
110
- # ---------------------------------------------------------
111
- # Comando utilitário: listar plugins
112
- # ---------------------------------------------------------
113
-
114
88
  @mt.command(name="plugins")
115
89
  def list_plugins():
116
90
  """
117
- Lista os plugins atualmente carregados no mtcli.
91
+ Lista os plugins carregados.
118
92
  """
119
93
 
120
94
  if not loaded_plugins:
@@ -127,9 +101,5 @@ def list_plugins():
127
101
  click.echo(f" {name}")
128
102
 
129
103
 
130
- # ---------------------------------------------------------
131
- # Entry point
132
- # ---------------------------------------------------------
133
-
134
104
  if __name__ == "__main__":
135
105
  mt()
mtcli/database.py CHANGED
@@ -1,3 +1,15 @@
1
+ """
2
+ Core de acesso ao banco SQLite do mtcli.
3
+
4
+ Responsável por:
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
11
+ """
12
+
1
13
  import sqlite3
2
14
  from pathlib import Path
3
15
  from datetime import datetime
@@ -7,6 +19,23 @@ BACKUP_DIR = Path.home() / ".mtcli" / "backups"
7
19
 
8
20
 
9
21
  def get_connection():
22
+ """
23
+ Cria ou retorna uma conexão SQLite otimizada para ingestão
24
+ contínua de ticks de mercado.
25
+
26
+ Configurações aplicadas:
27
+
28
+ - WAL (Write Ahead Logging)
29
+ - synchronous=NORMAL
30
+ - temp_store em memória
31
+ - mmap para leitura rápida
32
+ - cache expandido
33
+
34
+ Returns
35
+ -------
36
+ sqlite3.Connection
37
+ Conexão ativa com o banco SQLite.
38
+ """
10
39
 
11
40
  DB_PATH.parent.mkdir(parents=True, exist_ok=True)
12
41
  BACKUP_DIR.mkdir(parents=True, exist_ok=True)
@@ -16,8 +45,9 @@ def get_connection():
16
45
  conn.execute("PRAGMA journal_mode=WAL")
17
46
  conn.execute("PRAGMA synchronous=NORMAL")
18
47
  conn.execute("PRAGMA temp_store=MEMORY")
19
- conn.execute("PRAGMA mmap_size=30000000000")
48
+ conn.execute("PRAGMA mmap_size=268435456")
20
49
  conn.execute("PRAGMA cache_size=-200000")
50
+ conn.execute("PRAGMA journal_size_limit=67108864")
21
51
 
22
52
  conn.execute("""
23
53
  CREATE TABLE IF NOT EXISTS schema_migrations(
@@ -37,8 +67,12 @@ def get_connection():
37
67
 
38
68
  def wal_checkpoint(conn):
39
69
  """
40
- Reduz tamanho do WAL e mantém banco saudável.
70
+ Executa checkpoint do WAL.
71
+
72
+ Move os dados do arquivo `.wal` para o banco principal
73
+ e reduz seu tamanho.
41
74
  """
75
+
42
76
  conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
43
77
 
44
78
 
@@ -48,7 +82,10 @@ def wal_checkpoint(conn):
48
82
 
49
83
  def backup_database(conn):
50
84
  """
51
- Backup diário seguro do banco SQLite.
85
+ Realiza backup diário seguro do banco SQLite.
86
+
87
+ O backup utiliza a API nativa do SQLite,
88
+ permitindo cópia consistente mesmo com o banco em uso.
52
89
  """
53
90
 
54
91
  now = datetime.now().strftime("%Y%m%d")
@@ -1,5 +1,8 @@
1
1
  """
2
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.
3
6
  """
4
7
 
5
8
  from collections import deque
@@ -7,26 +10,43 @@ from collections import deque
7
10
 
8
11
  class TickCache:
9
12
  """
10
- Mantém janela recente de ticks em memória.
13
+ Mantém uma janela deslizante de ticks em memória.
14
+
15
+ Parameters
16
+ ----------
17
+ max_size : int
18
+ Número máximo de ticks armazenados no cache.
11
19
  """
12
20
 
13
21
  def __init__(self, max_size=10000):
14
22
  self.buffer = deque(maxlen=max_size)
15
23
 
16
24
  def add_many(self, ticks):
25
+ """
26
+ Adiciona múltiplos ticks ao cache.
27
+ """
17
28
 
18
29
  for t in ticks:
19
30
  self.buffer.append(t)
20
31
 
21
32
  def add(self, tick):
33
+ """
34
+ Adiciona um único tick ao cache.
35
+ """
22
36
 
23
37
  self.buffer.append(tick)
24
38
 
25
39
  def get_all(self):
40
+ """
41
+ Retorna todos os ticks do cache.
42
+ """
26
43
 
27
44
  return list(self.buffer)
28
45
 
29
46
  def get_last(self):
47
+ """
48
+ Retorna o último tick armazenado.
49
+ """
30
50
 
31
51
  if self.buffer:
32
52
  return self.buffer[-1]
@@ -34,5 +54,8 @@ class TickCache:
34
54
  return None
35
55
 
36
56
  def clear(self):
57
+ """
58
+ Limpa o cache.
59
+ """
37
60
 
38
61
  self.buffer.clear()
@@ -0,0 +1,93 @@
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,10 +1,11 @@
1
1
  """
2
- TickRepository profissional.
2
+ TickRepository.
3
3
 
4
4
  Responsável por:
5
5
 
6
6
  - Persistir ticks no SQLite
7
7
  - Sincronizar histórico inicial
8
+ - Consultas rápidas para engines (Renko etc)
8
9
  """
9
10
 
10
11
  import MetaTrader5 as mt5
@@ -32,6 +33,9 @@ class TickRepository:
32
33
  # ==========================================================
33
34
 
34
35
  def sync(self, symbol: str, days_back: int = 1):
36
+ """
37
+ Sincroniza histórico de ticks a partir do broker.
38
+ """
35
39
 
36
40
  total_inserted = 0
37
41
 
@@ -40,7 +44,7 @@ class TickRepository:
40
44
  last_msc = self._get_last_tick_msc(symbol)
41
45
 
42
46
  if last_msc:
43
- start = datetime.fromtimestamp(last_msc / 1000)
47
+ start = datetime.fromtimestamp((last_msc + 1) / 1000)
44
48
  else:
45
49
  start = datetime.now() - timedelta(days=days_back)
46
50
 
@@ -68,7 +72,7 @@ class TickRepository:
68
72
 
69
73
  last_msc = int(ticks[-1]["time_msc"])
70
74
 
71
- start = datetime.fromtimestamp(last_msc / 1000)
75
+ start = datetime.fromtimestamp((last_msc + 1) / 1000)
72
76
 
73
77
  if len(ticks) < self.BATCH_SIZE:
74
78
  break
@@ -106,23 +110,24 @@ class TickRepository:
106
110
 
107
111
  cursor.executemany(
108
112
  """
109
- INSERT OR IGNORE INTO ticks
110
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
113
+ INSERT INTO ticks(
114
+ symbol,time,time_msc,bid,ask,last,volume,flags
115
+ )
116
+ VALUES (?,?,?,?,?,?,?,?)
117
+ ON CONFLICT(symbol,time) DO NOTHING
111
118
  """,
112
119
  data,
113
120
  )
114
121
 
115
- inserted = cursor.rowcount
122
+ inserted = len(data)
116
123
 
117
124
  self.insert_counter += inserted
118
125
 
119
- # checkpoint periódico
120
126
  if self.insert_counter >= 200000:
121
127
 
122
128
  wal_checkpoint(self.conn)
123
129
  self.insert_counter = 0
124
130
 
125
- # backup diário
126
131
  today = datetime.now().date()
127
132
 
128
133
  if self.last_backup_day != today:
@@ -136,13 +141,34 @@ class TickRepository:
136
141
  # CONSULTAS
137
142
  # ==========================================================
138
143
 
144
+ def get_last_ticks(self, symbol, limit=5000):
145
+
146
+ cursor = self.conn.cursor()
147
+
148
+ cursor.execute(
149
+ """
150
+ SELECT time_msc,bid,ask,last,volume,flags
151
+ FROM ticks
152
+ WHERE symbol = ?
153
+ ORDER BY time_msc DESC
154
+ LIMIT ?
155
+ """,
156
+ (symbol, limit),
157
+ )
158
+
159
+ rows = cursor.fetchall()
160
+
161
+ rows.reverse()
162
+
163
+ return rows
164
+
139
165
  def get_ticks_between(self, symbol, start_msc, end_msc):
140
166
 
141
167
  cursor = self.conn.cursor()
142
168
 
143
169
  cursor.execute(
144
170
  """
145
- SELECT time_msc, bid, ask, last, volume, flags
171
+ SELECT time_msc,bid,ask,last,volume,flags
146
172
  FROM ticks
147
173
  WHERE symbol = ?
148
174
  AND time_msc BETWEEN ? AND ?
@@ -26,7 +26,7 @@ class TickStreamer:
26
26
 
27
27
  def start(self):
28
28
  """
29
- Inicia captura contínua.
29
+ Inicia captura contínua de ticks.
30
30
  """
31
31
 
32
32
  self.running = True
@@ -36,7 +36,7 @@ class TickStreamer:
36
36
  last_msc = self.repo._get_last_tick_msc(self.symbol)
37
37
 
38
38
  if last_msc:
39
- start = last_msc
39
+ start = last_msc + 1
40
40
  else:
41
41
  start = int(time.time() * 1000)
42
42
 
@@ -51,7 +51,7 @@ class TickStreamer:
51
51
 
52
52
  if ticks is None or len(ticks) == 0:
53
53
 
54
- time.sleep(0.05)
54
+ time.sleep(0.1)
55
55
  continue
56
56
 
57
57
  self.repo.conn.execute("BEGIN")
@@ -68,7 +68,7 @@ class TickStreamer:
68
68
 
69
69
  self.repo.conn.rollback()
70
70
 
71
- start = int(ticks[-1]["time_msc"])
71
+ start = int(ticks[-1]["time_msc"]) + 1
72
72
 
73
73
  def stop(self):
74
74
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mtcli
3
- Version: 3.8.0.dev0
3
+ Version: 3.8.0.dev2
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,5 +1,5 @@
1
1
  mtcli/__init__.py,sha256=guFO5EmCfuH8MNujJH8hw_oPg8Ee__QlQgVd-4A3N6o,84
2
- mtcli/cli.py,sha256=pKwKSAOMj8NWnw00JzMqDXJmGIucVuRZwSmO6f9VxHI,3335
2
+ mtcli/cli.py,sha256=RIrJZgpwqR_1ciTdg_Vs92PF1tuVvHaMG-IgYMqGkmY,2202
3
3
  mtcli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  mtcli/commands/bars.py,sha256=JXXq9HIeFU-VKf8e2rOKTMXx_h5SRcuvKER-mlOjxjM,4205
5
5
  mtcli/commands/conf.py,sha256=nYN3dk9hZVagkf3mP41yBdKUuCKlvzgaF8DHGKbG8OU,866
@@ -12,14 +12,15 @@ mtcli/data/__init__.py,sha256=NMzgUoiyY5OUFJkmR-OCFF2fOTY9vp-64z8-bh7HEcs,64
12
12
  mtcli/data/base.py,sha256=Hv1r1IlIhFVEhINWz90FZptYIdhyBKxhMBn5odJfT_8,314
13
13
  mtcli/data/csv.py,sha256=P007I9SoK51-lD_OXXjZu4NkKaGgNr2HRQ3pc_pS1XQ,1008
14
14
  mtcli/data/mt5.py,sha256=wsF14Vm5xXw0QbjHjC-tvXjjMADQWCrhkJ6ozbDLbzk,2880
15
- mtcli/database.py,sha256=ByMQVaqL9Uhwn8I_gF9hyPP_shhGhJqh4BRhPdlhB9s,1629
15
+ mtcli/database.py,sha256=P4MFZ2ytfhwN2rOe3jMSWxE26Eki8UHxa--a--0SayE,2480
16
16
  mtcli/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  mtcli/domain/timeframe.py,sha256=nG7vB01d1TyhInHaIZgCPYyVhbiklMIOSgxZnJ7MI6Q,2034
18
18
  mtcli/logger.py,sha256=fEj_meH9-H7CBm1qf1FYhs6QodP7KuYO4ptCyr8WlOc,3677
19
19
  mtcli/marketdata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- mtcli/marketdata/tick_cache.py,sha256=nC9LiuTpCMUDt04NH-ZK06os1W769wWpk66rx6kFSJ0,641
21
- mtcli/marketdata/tick_repository.py,sha256=PoXqTAyLoQFnP5iCM-Lc6pZiexKv-35d4JEU82-3Lfg,4422
22
- mtcli/marketdata/tick_streamer.py,sha256=-c45KN5bIaI6XLM2IxaaHpKh5cc_yY23Wf7cmvlzjIA,1631
20
+ mtcli/marketdata/tick_cache.py,sha256=T-auTfkhk5s-TZkaVaTuJGLEnaoxyED7yPr6CGyUDfM,1191
21
+ mtcli/marketdata/tick_engine.py,sha256=EySvnBVAG2kHu5NMT3fOajzV5GDR6i3Wb3mAUMzvQ0w,2156
22
+ mtcli/marketdata/tick_repository.py,sha256=SfQplU73ScSLQ0v7Uy-OAx7tQIWp2I7G7DBap64k768,5042
23
+ mtcli/marketdata/tick_streamer.py,sha256=K0zcpNEqc1zpFfc4YKl9tJcvOdWzXWO78D9C6A1zDuo,1647
23
24
  mtcli/migrations/001_initial_schema.py,sha256=Xve9r9RRAOtoZQ2iOaD-gdTfxSjaqoaCwGHn_ZSUtlg,439
24
25
  mtcli/migrations/002_ticks_time_msc.py,sha256=OKpxZZlb5QEKzVaYAt3kgi2vHu5CRoxLHQ4RRzYTX4E,420
25
26
  mtcli/migrations/003_optimize_ticks_without_rowid.py,sha256=AyOUJA1SvEhNVXnFRpz2FI4hKk1PutHuC6FSneceR4s,1234
@@ -73,8 +74,8 @@ mtcli/views/ranges_view.py,sha256=5gA2bAJuco-Xj964nsjs87tZ-079O7xKvaeif1UJ-PQ,12
73
74
  mtcli/views/rates_view.py,sha256=Jkdbg-Q_OVJuID-Q9HFUVICCF1jE82d-qfZWNm1JJ_4,1214
74
75
  mtcli/views/vars_view.py,sha256=Vpcej41qnZSvvSyvXYyW8W7CEFdqTxBd8NczV84XPJY,1635
75
76
  mtcli/views/volumes_view.py,sha256=d_6YxM0vA1revwoPwnwUjV19KcqnddVgdpBHTt5Vqms,1575
76
- mtcli-3.8.0.dev0.dist-info/entry_points.txt,sha256=EH8lMNy2L-TCJB3E3EJHF5QySz_b_86oOhcAKAN20-Q,103
77
- mtcli-3.8.0.dev0.dist-info/licenses/LICENSE,sha256=Z-2ANeRgM9IjXnHeg9mA2gillM6eTQj8sIExAGNe2-8,1092
78
- mtcli-3.8.0.dev0.dist-info/METADATA,sha256=eSjC_g_dM0tSbFqY0doJF82ChIUjOHhhXht_HheSEAU,3654
79
- mtcli-3.8.0.dev0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
80
- mtcli-3.8.0.dev0.dist-info/RECORD,,
77
+ mtcli-3.8.0.dev2.dist-info/entry_points.txt,sha256=EH8lMNy2L-TCJB3E3EJHF5QySz_b_86oOhcAKAN20-Q,103
78
+ mtcli-3.8.0.dev2.dist-info/licenses/LICENSE,sha256=Z-2ANeRgM9IjXnHeg9mA2gillM6eTQj8sIExAGNe2-8,1092
79
+ mtcli-3.8.0.dev2.dist-info/METADATA,sha256=znrOc5VBOdqB47AIvdoK0twd9YhgJRngCLZqSagHS6k,3654
80
+ mtcli-3.8.0.dev2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
81
+ mtcli-3.8.0.dev2.dist-info/RECORD,,