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.
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/PKG-INFO +1 -1
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/cli.py +31 -19
- mtcli-3.8.0.dev5/mtcli/commands/ticks.py +47 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/database.py +14 -32
- mtcli-3.8.0.dev5/mtcli/marketdata/tick_engine.py +134 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/marketdata/tick_repository.py +52 -60
- mtcli-3.8.0.dev5/mtcli/migrations/004_ticks_pk_time_msc.py +56 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/pyproject.toml +1 -1
- mtcli-3.8.0.dev3/mtcli/marketdata/tick_engine.py +0 -93
- mtcli-3.8.0.dev3/mtcli/marketdata/tick_streamer.py +0 -75
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/LICENSE +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/README.md +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/__init__.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/commands/__init__.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/commands/bars.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/commands/conf.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/commands/doctor.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/commands/migrate.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/conecta.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/conf.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/config_registre.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/data/__init__.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/data/base.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/data/csv.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/data/mt5.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/domain/__init__.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/domain/timeframe.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/logger.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/marketdata/__init__.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/marketdata/tick_cache.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/migrations/001_initial_schema.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/migrations/002_ticks_time_msc.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/migrations/003_optimize_ticks_without_rowid.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/migrations/__init__.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/migrations/runner.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/models/__init__.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/models/bar_model.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/models/bars_model.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/models/chart_model.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/models/conf_model.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/models/consecutive_bars_model.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/models/rates_model.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/models/signals_model.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/models/unconsecutive_bar_model.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/mt5_context.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugin.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugin_loader.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugin_manager.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/__init__.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/exemplo.py-dist +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/media_movel/__init__.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/media_movel/cli.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/media_movel/conf.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/media_movel/models/__init__.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/media_movel/models/model_media_movel.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/media_movel/tests/__init__.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/media_movel/tests/test_mm.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/media_movel/tests/test_model_media_movel.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/range_medio/__init__.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/range_medio/cli.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/range_medio/conf.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/range_medio/models/__init__.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/range_medio/models/average_range_model.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/range_medio/tests/__init__.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/range_medio/tests/test_rm.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/volume_medio/__init__.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/volume_medio/cli.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/volume_medio/conf.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/volume_medio/models/__init__.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/volume_medio/models/model_average_volume.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/volume_medio/tests/__init__.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/plugins/volume_medio/tests/test_vm.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/__init__.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/close_view.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/full_view.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/high_view.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/low_view.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/min_view.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/open_view.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/ranges_view.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/rates_view.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/vars_view.py +0 -0
- {mtcli-3.8.0.dev3 → mtcli-3.8.0.dev5}/mtcli/views/volumes_view.py +0 -0
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
+
Isso permite manter um histórico próprio de ticks
|
|
48
|
+
independente do histórico do broker.
|
|
30
49
|
"""
|
|
31
50
|
|
|
32
|
-
global
|
|
51
|
+
global _tick_engine
|
|
33
52
|
|
|
34
|
-
if
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
contínua de ticks
|
|
26
|
-
|
|
27
|
-
Configurações aplicadas:
|
|
27
|
+
Retorna conexão singleton SQLite otimizada para ingestão
|
|
28
|
+
contínua de ticks.
|
|
29
|
+
"""
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
- synchronous=NORMAL
|
|
31
|
-
- temp_store em memória
|
|
32
|
-
- mmap para leitura rápida
|
|
33
|
-
- cache expandido
|
|
31
|
+
global _connection
|
|
34
32
|
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
39
|
+
end = datetime.now()
|
|
40
|
+
|
|
41
|
+
last_msc = self._get_last_tick_msc(symbol)
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
if last_msc:
|
|
44
|
+
start = datetime.fromtimestamp((last_msc + 1) / 1000)
|
|
45
|
+
else:
|
|
46
|
+
start = end - timedelta(days=days_back)
|
|
45
47
|
|
|
46
|
-
|
|
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
|
-
|
|
50
|
+
with mt5_conexao():
|
|
51
|
+
|
|
52
|
+
while start < end:
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
chunk_end = min(start + window, end)
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
ticks = mt5.copy_ticks_range(
|
|
57
|
+
symbol,
|
|
58
|
+
start,
|
|
59
|
+
chunk_end,
|
|
60
|
+
mt5.COPY_TICKS_ALL
|
|
61
|
+
)
|
|
56
62
|
|
|
57
|
-
|
|
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
|
-
|
|
65
|
-
break
|
|
65
|
+
self.conn.execute("BEGIN")
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
try:
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
inserted = self._insert_ticks(symbol, ticks)
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
total_inserted += inserted
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
self.cache.add_many(ticks)
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
self.conn.commit()
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
break
|
|
77
|
+
except Exception:
|
|
79
78
|
|
|
80
|
-
|
|
79
|
+
self.conn.rollback()
|
|
80
|
+
raise
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
start = chunk_end
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
raise
|
|
84
|
+
self._daily_backup()
|
|
86
85
|
|
|
87
86
|
return total_inserted
|
|
88
87
|
|
|
89
88
|
# ==========================================================
|
|
90
|
-
#
|
|
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,
|
|
116
|
+
ON CONFLICT(symbol,time_msc) DO NOTHING
|
|
118
117
|
""",
|
|
119
118
|
data,
|
|
120
119
|
)
|
|
121
120
|
|
|
122
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
|
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.dev3 → mtcli-3.8.0.dev5}/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.dev3 → mtcli-3.8.0.dev5}/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.dev3 → mtcli-3.8.0.dev5}/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
|