mtcli 3.8.0.dev0__tar.gz → 3.8.0.dev2__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.
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/PKG-INFO +1 -1
- mtcli-3.8.0.dev2/mtcli/cli.py +105 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/database.py +40 -3
- mtcli-3.8.0.dev2/mtcli/marketdata/tick_cache.py +61 -0
- mtcli-3.8.0.dev2/mtcli/marketdata/tick_engine.py +93 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/marketdata/tick_repository.py +35 -9
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/marketdata/tick_streamer.py +4 -4
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/pyproject.toml +1 -1
- mtcli-3.8.0.dev0/mtcli/cli.py +0 -135
- mtcli-3.8.0.dev0/mtcli/marketdata/tick_cache.py +0 -38
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/LICENSE +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/README.md +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/__init__.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/commands/__init__.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/commands/bars.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/commands/conf.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/commands/doctor.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/commands/migrate.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/conecta.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/conf.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/config_registre.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/data/__init__.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/data/base.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/data/csv.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/data/mt5.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/domain/__init__.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/domain/timeframe.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/logger.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/marketdata/__init__.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/migrations/001_initial_schema.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/migrations/002_ticks_time_msc.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/migrations/003_optimize_ticks_without_rowid.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/migrations/__init__.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/migrations/runner.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/models/__init__.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/models/bar_model.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/models/bars_model.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/models/chart_model.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/models/conf_model.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/models/consecutive_bars_model.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/models/rates_model.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/models/signals_model.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/models/unconsecutive_bar_model.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/mt5_context.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugin.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugin_loader.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugin_manager.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/__init__.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/exemplo.py-dist +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/media_movel/__init__.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/media_movel/cli.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/media_movel/conf.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/media_movel/models/__init__.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/media_movel/models/model_media_movel.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/media_movel/tests/__init__.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/media_movel/tests/test_mm.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/media_movel/tests/test_model_media_movel.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/range_medio/__init__.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/range_medio/cli.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/range_medio/conf.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/range_medio/models/__init__.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/range_medio/models/average_range_model.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/range_medio/tests/__init__.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/range_medio/tests/test_rm.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/volume_medio/__init__.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/volume_medio/cli.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/volume_medio/conf.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/volume_medio/models/__init__.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/volume_medio/models/model_average_volume.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/volume_medio/tests/__init__.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/volume_medio/tests/test_vm.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/views/__init__.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/views/close_view.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/views/full_view.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/views/high_view.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/views/low_view.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/views/min_view.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/views/open_view.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/views/ranges_view.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/views/rates_view.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/views/vars_view.py +0 -0
- {mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/views/volumes_view.py +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI principal do mtcli.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import threading
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from mtcli.plugin_loader import load_plugins
|
|
10
|
+
from mtcli.logger import setup_logger
|
|
11
|
+
from mtcli.marketdata.tick_streamer import TickStreamer
|
|
12
|
+
|
|
13
|
+
from .commands.bars import bars
|
|
14
|
+
from .commands.doctor import doctor
|
|
15
|
+
from .commands.migrate import migrate
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = setup_logger(__name__)
|
|
19
|
+
|
|
20
|
+
_tick_streamer = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def start_tick_capture():
|
|
24
|
+
"""
|
|
25
|
+
Inicia captura contínua de ticks em background.
|
|
26
|
+
|
|
27
|
+
O símbolo deve ser definido via variável:
|
|
28
|
+
|
|
29
|
+
MTCLI_SYMBOL=WINJ26
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
global _tick_streamer
|
|
33
|
+
|
|
34
|
+
if _tick_streamer:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
symbol = os.getenv("MTCLI_SYMBOL")
|
|
38
|
+
|
|
39
|
+
if not symbol:
|
|
40
|
+
logger.info("Captura de ticks desativada (MTCLI_SYMBOL não definido).")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
logger.info("Iniciando captura contínua de ticks para %s", symbol)
|
|
44
|
+
|
|
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
|
+
)
|
|
54
|
+
|
|
55
|
+
thread.start()
|
|
56
|
+
|
|
57
|
+
logger.info("Captura de ticks iniciada em background.")
|
|
58
|
+
|
|
59
|
+
except Exception:
|
|
60
|
+
|
|
61
|
+
logger.exception("Falha ao iniciar captura de ticks")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@click.group(context_settings={"max_content_width": 120}, invoke_without_command=True)
|
|
65
|
+
@click.version_option(package_name="mtcli")
|
|
66
|
+
@click.pass_context
|
|
67
|
+
def mt(ctx):
|
|
68
|
+
"""
|
|
69
|
+
CLI principal do mtcli.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
start_tick_capture()
|
|
73
|
+
|
|
74
|
+
if ctx.invoked_subcommand is None:
|
|
75
|
+
click.echo(ctx.get_help())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
mt.add_command(doctor, name="doctor")
|
|
79
|
+
mt.add_command(bars, name="bars")
|
|
80
|
+
mt.add_command(doctor, name="dr")
|
|
81
|
+
mt.add_command(migrate)
|
|
82
|
+
|
|
83
|
+
loaded_plugins = load_plugins(mt)
|
|
84
|
+
|
|
85
|
+
logger.info("Plugins carregados: %s", loaded_plugins)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@mt.command(name="plugins")
|
|
89
|
+
def list_plugins():
|
|
90
|
+
"""
|
|
91
|
+
Lista os plugins carregados.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
if not loaded_plugins:
|
|
95
|
+
click.echo("Nenhum plugin carregado.")
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
click.echo("Plugins carregados:\n")
|
|
99
|
+
|
|
100
|
+
for name in loaded_plugins:
|
|
101
|
+
click.echo(f" {name}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
mt()
|
|
@@ -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=
|
|
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
|
-
|
|
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
|
-
|
|
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")
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
|
|
8
|
+
from collections import deque
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TickCache:
|
|
12
|
+
"""
|
|
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.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, max_size=10000):
|
|
22
|
+
self.buffer = deque(maxlen=max_size)
|
|
23
|
+
|
|
24
|
+
def add_many(self, ticks):
|
|
25
|
+
"""
|
|
26
|
+
Adiciona múltiplos ticks ao cache.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
for t in ticks:
|
|
30
|
+
self.buffer.append(t)
|
|
31
|
+
|
|
32
|
+
def add(self, tick):
|
|
33
|
+
"""
|
|
34
|
+
Adiciona um único tick ao cache.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
self.buffer.append(tick)
|
|
38
|
+
|
|
39
|
+
def get_all(self):
|
|
40
|
+
"""
|
|
41
|
+
Retorna todos os ticks do cache.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
return list(self.buffer)
|
|
45
|
+
|
|
46
|
+
def get_last(self):
|
|
47
|
+
"""
|
|
48
|
+
Retorna o último tick armazenado.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
if self.buffer:
|
|
52
|
+
return self.buffer[-1]
|
|
53
|
+
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
def clear(self):
|
|
57
|
+
"""
|
|
58
|
+
Limpa o cache.
|
|
59
|
+
"""
|
|
60
|
+
|
|
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
|
|
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
|
|
110
|
-
|
|
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 =
|
|
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,
|
|
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.
|
|
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
|
|
mtcli-3.8.0.dev0/mtcli/cli.py
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
"""
|
|
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
|
-
"""
|
|
10
|
-
|
|
11
|
-
import os
|
|
12
|
-
import threading
|
|
13
|
-
import click
|
|
14
|
-
|
|
15
|
-
from mtcli.plugin_loader import load_plugins
|
|
16
|
-
from mtcli.logger import setup_logger
|
|
17
|
-
from mtcli.marketdata.tick_streamer import TickStreamer
|
|
18
|
-
|
|
19
|
-
from .commands.bars import bars
|
|
20
|
-
from .commands.doctor import doctor
|
|
21
|
-
from .commands.migrate import migrate
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
logger = setup_logger(__name__)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# ---------------------------------------------------------
|
|
28
|
-
# Configuração de captura de ticks
|
|
29
|
-
# ---------------------------------------------------------
|
|
30
|
-
|
|
31
|
-
_tick_streamer = None
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def start_tick_capture():
|
|
35
|
-
"""
|
|
36
|
-
Inicia captura contínua de ticks em background.
|
|
37
|
-
|
|
38
|
-
O símbolo pode ser configurado via variável de ambiente:
|
|
39
|
-
|
|
40
|
-
MTCLI_SYMBOL=WINJ26
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
global _tick_streamer
|
|
44
|
-
|
|
45
|
-
if _tick_streamer:
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
symbol = os.getenv("SYMBOL")
|
|
49
|
-
|
|
50
|
-
if not symbol:
|
|
51
|
-
logger.info("Captura de ticks desativada (MTCLI_SYMBOL não definido).")
|
|
52
|
-
return
|
|
53
|
-
|
|
54
|
-
logger.info("Iniciando captura contínua de ticks para %s", symbol)
|
|
55
|
-
|
|
56
|
-
_tick_streamer = TickStreamer(symbol)
|
|
57
|
-
|
|
58
|
-
thread = threading.Thread(
|
|
59
|
-
target=_tick_streamer.start,
|
|
60
|
-
daemon=True,
|
|
61
|
-
name="mtcli-tick-streamer",
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
thread.start()
|
|
65
|
-
|
|
66
|
-
logger.info("Captura de ticks iniciada em background.")
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# ---------------------------------------------------------
|
|
70
|
-
# CLI principal
|
|
71
|
-
# ---------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
@click.group(context_settings={"max_content_width": 120}, invoke_without_command=True)
|
|
74
|
-
@click.version_option(package_name="mtcli")
|
|
75
|
-
@click.pass_context
|
|
76
|
-
def mt(ctx):
|
|
77
|
-
"""
|
|
78
|
-
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
|
-
"""
|
|
83
|
-
|
|
84
|
-
# inicia captura de ticks
|
|
85
|
-
start_tick_capture()
|
|
86
|
-
|
|
87
|
-
if ctx.invoked_subcommand is None:
|
|
88
|
-
click.echo(ctx.get_help())
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
# ---------------------------------------------------------
|
|
92
|
-
# Comandos internos
|
|
93
|
-
# ---------------------------------------------------------
|
|
94
|
-
|
|
95
|
-
mt.add_command(doctor, name="doctor")
|
|
96
|
-
mt.add_command(bars, name="bars")
|
|
97
|
-
mt.add_command(doctor, name="dr")
|
|
98
|
-
mt.add_command(migrate)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
# ---------------------------------------------------------
|
|
102
|
-
# Carregamento de plugins
|
|
103
|
-
# ---------------------------------------------------------
|
|
104
|
-
|
|
105
|
-
loaded_plugins = load_plugins(mt)
|
|
106
|
-
|
|
107
|
-
logger.info("Plugins carregados: %s", loaded_plugins)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
# ---------------------------------------------------------
|
|
111
|
-
# Comando utilitário: listar plugins
|
|
112
|
-
# ---------------------------------------------------------
|
|
113
|
-
|
|
114
|
-
@mt.command(name="plugins")
|
|
115
|
-
def list_plugins():
|
|
116
|
-
"""
|
|
117
|
-
Lista os plugins atualmente carregados no mtcli.
|
|
118
|
-
"""
|
|
119
|
-
|
|
120
|
-
if not loaded_plugins:
|
|
121
|
-
click.echo("Nenhum plugin carregado.")
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
click.echo("Plugins carregados:\n")
|
|
125
|
-
|
|
126
|
-
for name in loaded_plugins:
|
|
127
|
-
click.echo(f" {name}")
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
# ---------------------------------------------------------
|
|
131
|
-
# Entry point
|
|
132
|
-
# ---------------------------------------------------------
|
|
133
|
-
|
|
134
|
-
if __name__ == "__main__":
|
|
135
|
-
mt()
|
|
@@ -1,38 +0,0 @@
|
|
|
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
|
-
|
|
18
|
-
for t in ticks:
|
|
19
|
-
self.buffer.append(t)
|
|
20
|
-
|
|
21
|
-
def add(self, tick):
|
|
22
|
-
|
|
23
|
-
self.buffer.append(tick)
|
|
24
|
-
|
|
25
|
-
def get_all(self):
|
|
26
|
-
|
|
27
|
-
return list(self.buffer)
|
|
28
|
-
|
|
29
|
-
def get_last(self):
|
|
30
|
-
|
|
31
|
-
if self.buffer:
|
|
32
|
-
return self.buffer[-1]
|
|
33
|
-
|
|
34
|
-
return None
|
|
35
|
-
|
|
36
|
-
def clear(self):
|
|
37
|
-
|
|
38
|
-
self.buffer.clear()
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
{mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/media_movel/tests/test_model_media_movel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/range_medio/models/average_range_model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mtcli-3.8.0.dev0 → mtcli-3.8.0.dev2}/mtcli/plugins/volume_medio/models/model_average_volume.py
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|