mtcli 3.8.0.dev10__tar.gz → 3.8.0.dev12__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.dev10 → mtcli-3.8.0.dev12}/PKG-INFO +1 -1
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/cli.py +8 -4
- mtcli-3.8.0.dev12/mtcli/commands/backfill.py +68 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/commands/ticks.py +9 -12
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/database.py +12 -22
- mtcli-3.8.0.dev12/mtcli/marketdata/backfill_engine.py +201 -0
- mtcli-3.8.0.dev12/mtcli/marketdata/tick_bus.py +45 -0
- mtcli-3.8.0.dev12/mtcli/marketdata/tick_cache.py +160 -0
- mtcli-3.8.0.dev12/mtcli/marketdata/tick_engine.py +151 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/marketdata/tick_repository.py +127 -32
- mtcli-3.8.0.dev12/mtcli/marketdata/tick_writer.py +38 -0
- mtcli-3.8.0.dev12/mtcli/services/__init__.py +0 -0
- mtcli-3.8.0.dev12/mtcli/services/maintenance_service.py +170 -0
- mtcli-3.8.0.dev12/mtcli/services/tick_service.py +82 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/pyproject.toml +1 -1
- mtcli-3.8.0.dev10/mtcli/marketdata/tick_cache.py +0 -61
- mtcli-3.8.0.dev10/mtcli/marketdata/tick_engine.py +0 -133
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/LICENSE +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/README.md +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/__main__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/cli_dev.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/commands/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/commands/bars.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/commands/conf.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/commands/doctor.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/commands_dev/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/commands_dev/migrate.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/conecta.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/conf.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/config_registre.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/data/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/data/base.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/data/csv.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/data/mt5.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/domain/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/domain/timeframe.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/logger.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/marketdata/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/migrations/001_initial_schema.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/migrations/002_ticks_time_msc.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/migrations/003_optimize_ticks_without_rowid.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/migrations/004_ticks_pk_time_msc.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/migrations/005_tick_price_compression.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/migrations/006_add_index_ticks_symbol_time.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/migrations/007_deduplicate_ticks_and_add_unique_index.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/migrations/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/migrations/__main__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/migrations/runner.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/models/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/models/bar_model.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/models/bars_model.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/models/chart_model.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/models/conf_model.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/models/consecutive_bars_model.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/models/rates_model.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/models/signals_model.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/models/unconsecutive_bar_model.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/mt5_context.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugin.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugin_loader.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugin_manager.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/exemplo.py-dist +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/media_movel/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/media_movel/cli.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/media_movel/conf.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/media_movel/models/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/media_movel/models/model_media_movel.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/media_movel/tests/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/media_movel/tests/test_mm.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/media_movel/tests/test_model_media_movel.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/range_medio/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/range_medio/cli.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/range_medio/conf.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/range_medio/models/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/range_medio/models/average_range_model.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/range_medio/tests/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/range_medio/tests/test_rm.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/volume_medio/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/volume_medio/cli.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/volume_medio/conf.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/volume_medio/models/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/volume_medio/models/model_average_volume.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/volume_medio/tests/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/plugins/volume_medio/tests/test_vm.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/views/__init__.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/views/close_view.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/views/full_view.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/views/high_view.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/views/low_view.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/views/min_view.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/views/open_view.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/views/ranges_view.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/views/rates_view.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/views/vars_view.py +0 -0
- {mtcli-3.8.0.dev10 → mtcli-3.8.0.dev12}/mtcli/views/volumes_view.py +0 -0
|
@@ -23,11 +23,12 @@ import click
|
|
|
23
23
|
|
|
24
24
|
from mtcli.plugin_loader import load_plugins
|
|
25
25
|
from mtcli.logger import setup_logger
|
|
26
|
-
from mtcli.
|
|
26
|
+
from mtcli.services.tick_service import ensure_tick_engine
|
|
27
27
|
|
|
28
28
|
from .commands.bars import bars
|
|
29
29
|
from .commands.doctor import doctor
|
|
30
30
|
from .commands.ticks import ticks
|
|
31
|
+
from .commands.backfill import backfill
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
logger = setup_logger(__name__)
|
|
@@ -55,15 +56,17 @@ def start_tick_capture():
|
|
|
55
56
|
symbol = os.getenv("MTCLI_SYMBOL")
|
|
56
57
|
|
|
57
58
|
if not symbol:
|
|
58
|
-
logger.info(
|
|
59
|
+
logger.info(
|
|
60
|
+
"Captura automática de ticks desativada (MTCLI_SYMBOL não definido)."
|
|
61
|
+
)
|
|
59
62
|
return
|
|
60
63
|
|
|
61
64
|
logger.info("Iniciando captura contínua de ticks para %s", symbol)
|
|
62
65
|
|
|
63
66
|
try:
|
|
64
67
|
|
|
65
|
-
|
|
66
|
-
_tick_engine
|
|
68
|
+
# CORREÇÃO PRINCIPAL
|
|
69
|
+
_tick_engine = ensure_tick_engine([symbol])
|
|
67
70
|
|
|
68
71
|
logger.info("Captura de ticks iniciada em background.")
|
|
69
72
|
|
|
@@ -89,6 +92,7 @@ mt.add_command(doctor, name="doctor")
|
|
|
89
92
|
mt.add_command(bars, name="bars")
|
|
90
93
|
mt.add_command(doctor, name="dr")
|
|
91
94
|
mt.add_command(ticks)
|
|
95
|
+
mt.add_command(backfill, name="fill")
|
|
92
96
|
|
|
93
97
|
loaded_plugins = load_plugins(mt)
|
|
94
98
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comando CLI: backfill
|
|
3
|
+
|
|
4
|
+
Responsável por carregar histórico de ticks do MetaTrader5
|
|
5
|
+
para o banco SQLite do mtcli utilizando o BackfillEngine.
|
|
6
|
+
|
|
7
|
+
Fluxo:
|
|
8
|
+
|
|
9
|
+
BackfillEngine
|
|
10
|
+
↓
|
|
11
|
+
TickRepository
|
|
12
|
+
↓
|
|
13
|
+
SQLite
|
|
14
|
+
|
|
15
|
+
Opcionalmente os ticks também podem ser publicados no TickBus
|
|
16
|
+
para que plugins consumam o fluxo histórico.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
|
|
21
|
+
from mtcli.marketdata.backfill_engine import BackfillEngine
|
|
22
|
+
from mtcli.marketdata.tick_bus import TickBus
|
|
23
|
+
from mtcli.marketdata.tick_repository import TickRepository
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@click.command()
|
|
27
|
+
@click.argument("symbol")
|
|
28
|
+
@click.option(
|
|
29
|
+
"--days",
|
|
30
|
+
default=5,
|
|
31
|
+
show_default=True,
|
|
32
|
+
help="Número de dias de histórico a carregar caso não exista histórico local.",
|
|
33
|
+
)
|
|
34
|
+
def backfill(symbol: str, days: int):
|
|
35
|
+
"""
|
|
36
|
+
Executa backfill histórico de ticks.
|
|
37
|
+
|
|
38
|
+
Este comando baixa ticks históricos diretamente do MetaTrader5
|
|
39
|
+
e os grava no banco local SQLite do mtcli.
|
|
40
|
+
|
|
41
|
+
O processo é incremental:
|
|
42
|
+
|
|
43
|
+
- se já existirem ticks no banco, o backfill continua do último tick
|
|
44
|
+
- caso contrário, carrega o número de dias definido em --days
|
|
45
|
+
|
|
46
|
+
Examples
|
|
47
|
+
--------
|
|
48
|
+
|
|
49
|
+
Carregar 5 dias:
|
|
50
|
+
|
|
51
|
+
mt fill WINJ26
|
|
52
|
+
|
|
53
|
+
Carregar 30 dias:
|
|
54
|
+
|
|
55
|
+
mt fill WINJ26 --days 30
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
# Event bus (permite que plugins consumam os ticks históricos)
|
|
59
|
+
bus = TickBus()
|
|
60
|
+
|
|
61
|
+
# Repositório de persistência
|
|
62
|
+
repo = TickRepository()
|
|
63
|
+
|
|
64
|
+
# Engine de backfill
|
|
65
|
+
engine = BackfillEngine(symbol, bus, repo)
|
|
66
|
+
|
|
67
|
+
# Executa o carregamento histórico
|
|
68
|
+
engine.run(days)
|
|
@@ -1,45 +1,42 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Comando CLI para captura contínua de ticks.
|
|
3
3
|
|
|
4
|
-
Permite iniciar
|
|
4
|
+
Permite iniciar o serviço de captura de ticks
|
|
5
5
|
para um ou mais símbolos.
|
|
6
6
|
|
|
7
7
|
Exemplo:
|
|
8
8
|
|
|
9
9
|
mt ticks WIN$N
|
|
10
10
|
mt ticks WIN$N WDO$N PETR4
|
|
11
|
-
|
|
12
|
-
A captura continua até o usuário interromper
|
|
13
|
-
com Ctrl+C.
|
|
14
11
|
"""
|
|
15
12
|
|
|
16
13
|
import time
|
|
17
14
|
import click
|
|
18
15
|
|
|
19
|
-
from mtcli.
|
|
16
|
+
from mtcli.services.tick_service import ensure_tick_engine
|
|
20
17
|
|
|
21
18
|
|
|
22
19
|
@click.command()
|
|
23
20
|
@click.argument("symbols", nargs=-1)
|
|
24
21
|
def ticks(symbols):
|
|
25
22
|
"""
|
|
26
|
-
Inicia captura contínua de ticks
|
|
23
|
+
Inicia captura contínua de ticks.
|
|
27
24
|
"""
|
|
28
25
|
|
|
29
26
|
if not symbols:
|
|
30
27
|
click.echo("Informe ao menos um símbolo.")
|
|
31
28
|
return
|
|
32
29
|
|
|
33
|
-
engine
|
|
30
|
+
# atualmente o engine suporta apenas 1 símbolo
|
|
31
|
+
symbol = symbols[0]
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
engine = ensure_tick_engine(symbol)
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
click.echo(f"Captura de ticks iniciada para {symbol}")
|
|
38
36
|
|
|
39
37
|
try:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
time.sleep(1)
|
|
38
|
+
# inicia engine
|
|
39
|
+
engine.start()
|
|
43
40
|
|
|
44
41
|
except KeyboardInterrupt:
|
|
45
42
|
|
|
@@ -3,17 +3,18 @@ Core de acesso ao banco SQLite do mtcli.
|
|
|
3
3
|
|
|
4
4
|
Responsável por:
|
|
5
5
|
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
- Backup e manutenção do banco
|
|
6
|
+
- criar conexão SQLite
|
|
7
|
+
- aplicar otimizações
|
|
8
|
+
- executar migrations automaticamente
|
|
9
|
+
- backup automático
|
|
11
10
|
"""
|
|
12
11
|
|
|
13
12
|
import sqlite3
|
|
14
13
|
from pathlib import Path
|
|
15
14
|
from datetime import datetime
|
|
15
|
+
|
|
16
16
|
from .conf import DB_NAME
|
|
17
|
+
from .migrations.runner import run_migrations
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
DB_PATH = Path.home() / ".mtcli" / DB_NAME
|
|
@@ -24,8 +25,7 @@ _connection = None
|
|
|
24
25
|
|
|
25
26
|
def get_connection():
|
|
26
27
|
"""
|
|
27
|
-
Retorna conexão singleton SQLite
|
|
28
|
-
contínua de ticks.
|
|
28
|
+
Retorna conexão singleton SQLite.
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
31
|
global _connection
|
|
@@ -38,25 +38,18 @@ def get_connection():
|
|
|
38
38
|
|
|
39
39
|
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
|
40
40
|
|
|
41
|
+
# otimizações
|
|
41
42
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
42
43
|
conn.execute("PRAGMA synchronous=NORMAL")
|
|
43
44
|
conn.execute("PRAGMA temp_store=MEMORY")
|
|
44
45
|
conn.execute("PRAGMA mmap_size=30000000000")
|
|
45
46
|
conn.execute("PRAGMA cache_size=-200000")
|
|
46
47
|
conn.execute("PRAGMA journal_size_limit=67108864")
|
|
47
|
-
conn.execute("PRAGMA
|
|
48
|
-
|
|
49
|
-
# checkpoint automático
|
|
50
|
-
conn.execute("PRAGMA wal_autocheckpoint=1000")
|
|
51
|
-
|
|
52
|
-
conn.execute("""
|
|
53
|
-
CREATE TABLE IF NOT EXISTS schema_migrations(
|
|
54
|
-
version INTEGER PRIMARY KEY,
|
|
55
|
-
applied_at TEXT NOT NULL
|
|
56
|
-
)
|
|
57
|
-
""")
|
|
48
|
+
conn.execute("PRAGMA wal_autocheckpoint=5000")
|
|
49
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
58
50
|
|
|
59
|
-
|
|
51
|
+
# executa migrations automaticamente
|
|
52
|
+
run_migrations(conn)
|
|
60
53
|
|
|
61
54
|
_connection = conn
|
|
62
55
|
|
|
@@ -68,9 +61,6 @@ def get_connection():
|
|
|
68
61
|
# ==========================================================
|
|
69
62
|
|
|
70
63
|
def backup_database(conn):
|
|
71
|
-
"""
|
|
72
|
-
Realiza backup diário seguro do banco SQLite.
|
|
73
|
-
"""
|
|
74
64
|
|
|
75
65
|
now = datetime.now().strftime("%Y%m%d")
|
|
76
66
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BackfillEngine
|
|
3
|
+
|
|
4
|
+
Carrega ticks históricos do MetaTrader5 de forma eficiente
|
|
5
|
+
e os publica no TickBus para processamento.
|
|
6
|
+
|
|
7
|
+
Características:
|
|
8
|
+
|
|
9
|
+
• leitura em chunks grandes (100k ticks)
|
|
10
|
+
• proteção contra duplicação
|
|
11
|
+
• compatível com TickBus
|
|
12
|
+
• inserção em batch no SQLite
|
|
13
|
+
• sem barra de progresso (CLI acessível)
|
|
14
|
+
• baixo uso de memória
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import datetime
|
|
18
|
+
import MetaTrader5 as mt5
|
|
19
|
+
|
|
20
|
+
from mtcli.logger import setup_logger
|
|
21
|
+
|
|
22
|
+
logger = setup_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BackfillEngine:
|
|
26
|
+
"""
|
|
27
|
+
Engine responsável pelo carregamento histórico de ticks.
|
|
28
|
+
|
|
29
|
+
O BackfillEngine baixa ticks históricos diretamente do MT5
|
|
30
|
+
utilizando janelas grandes e publica os ticks no TickBus,
|
|
31
|
+
permitindo que múltiplos subscribers processem os dados.
|
|
32
|
+
|
|
33
|
+
Tipicamente os subscribers incluem:
|
|
34
|
+
|
|
35
|
+
- TickWriter (persistência em banco)
|
|
36
|
+
- plugins de análise
|
|
37
|
+
- geradores de estruturas derivadas (Renko, bars, etc)
|
|
38
|
+
|
|
39
|
+
Fluxo arquitetural:
|
|
40
|
+
|
|
41
|
+
MetaTrader5
|
|
42
|
+
↓
|
|
43
|
+
BackfillEngine
|
|
44
|
+
↓
|
|
45
|
+
TickBus
|
|
46
|
+
↓
|
|
47
|
+
Subscribers
|
|
48
|
+
↓
|
|
49
|
+
SQLite / Plugins
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
CHUNK_SIZE = 100_000
|
|
53
|
+
|
|
54
|
+
def __init__(self, symbol, tick_bus, repository):
|
|
55
|
+
"""
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
symbol : str
|
|
59
|
+
Símbolo a carregar (ex: WINJ26)
|
|
60
|
+
|
|
61
|
+
tick_bus : TickBus
|
|
62
|
+
Event bus responsável pela distribuição dos ticks.
|
|
63
|
+
|
|
64
|
+
repository : TickRepository
|
|
65
|
+
Repositório responsável pela persistência.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
self.symbol = symbol
|
|
69
|
+
self.tick_bus = tick_bus
|
|
70
|
+
self.repository = repository
|
|
71
|
+
|
|
72
|
+
self.last_time_msc = None
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------
|
|
75
|
+
# util
|
|
76
|
+
# ---------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
def _get_last_stored(self):
|
|
79
|
+
"""
|
|
80
|
+
Consulta o último tick armazenado no banco.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
int or None
|
|
85
|
+
Timestamp em milissegundos do último tick armazenado.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
last = self.repository._get_last_tick_msc(self.symbol)
|
|
89
|
+
|
|
90
|
+
if last:
|
|
91
|
+
logger.info(
|
|
92
|
+
"Backfill retomando do último tick armazenado: %s",
|
|
93
|
+
last,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return last
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------
|
|
99
|
+
# main
|
|
100
|
+
# ---------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def run(self, days=5):
|
|
103
|
+
"""
|
|
104
|
+
Executa o processo de backfill.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
days : int
|
|
109
|
+
Número de dias de histórico caso não exista
|
|
110
|
+
histórico local no banco.
|
|
111
|
+
|
|
112
|
+
Notes
|
|
113
|
+
-----
|
|
114
|
+
O processo é incremental:
|
|
115
|
+
|
|
116
|
+
- se o banco possuir ticks, continua a partir do último
|
|
117
|
+
- caso contrário inicia no passado definido por `days`
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
logger.info(
|
|
121
|
+
"Backfill iniciado (%s) — até %s dias de histórico",
|
|
122
|
+
self.symbol,
|
|
123
|
+
days,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if not mt5.initialize():
|
|
127
|
+
logger.error("Falha ao inicializar MetaTrader5")
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
mt5.symbol_select(self.symbol, True)
|
|
131
|
+
|
|
132
|
+
now = datetime.datetime.utcnow()
|
|
133
|
+
|
|
134
|
+
last = self._get_last_stored()
|
|
135
|
+
|
|
136
|
+
if last:
|
|
137
|
+
start = datetime.datetime.fromtimestamp(last / 1000)
|
|
138
|
+
self.last_time_msc = last
|
|
139
|
+
else:
|
|
140
|
+
start = now - datetime.timedelta(days=days)
|
|
141
|
+
|
|
142
|
+
total_loaded = 0
|
|
143
|
+
|
|
144
|
+
while True:
|
|
145
|
+
|
|
146
|
+
ticks = mt5.copy_ticks_from(
|
|
147
|
+
self.symbol,
|
|
148
|
+
start,
|
|
149
|
+
self.CHUNK_SIZE,
|
|
150
|
+
mt5.COPY_TICKS_ALL,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if ticks is None or len(ticks) == 0:
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------
|
|
157
|
+
# filtro de duplicação (vetorizado numpy)
|
|
158
|
+
# ---------------------------------------------
|
|
159
|
+
|
|
160
|
+
if self.last_time_msc:
|
|
161
|
+
|
|
162
|
+
mask = ticks["time_msc"] > self.last_time_msc
|
|
163
|
+
ticks = ticks[mask]
|
|
164
|
+
|
|
165
|
+
if len(ticks) == 0:
|
|
166
|
+
break
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------
|
|
169
|
+
# publica no event bus
|
|
170
|
+
# ---------------------------------------------
|
|
171
|
+
|
|
172
|
+
for i in range(len(ticks)):
|
|
173
|
+
self.tick_bus.publish(ticks[i])
|
|
174
|
+
|
|
175
|
+
# ---------------------------------------------
|
|
176
|
+
# grava no banco
|
|
177
|
+
# ---------------------------------------------
|
|
178
|
+
|
|
179
|
+
inserted = self.repository.insert_ticks(
|
|
180
|
+
self.symbol,
|
|
181
|
+
ticks
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
total_loaded += inserted
|
|
185
|
+
|
|
186
|
+
# último tick do chunk
|
|
187
|
+
self.last_time_msc = int(ticks["time_msc"][-1])
|
|
188
|
+
|
|
189
|
+
# próximo ponto de leitura
|
|
190
|
+
start = datetime.datetime.fromtimestamp(
|
|
191
|
+
self.last_time_msc / 1000
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if len(ticks) < self.CHUNK_SIZE:
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
logger.info(
|
|
198
|
+
"Backfill concluído (%s) — %s ticks inseridos",
|
|
199
|
+
self.symbol,
|
|
200
|
+
f"{total_loaded:,}",
|
|
201
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TickBus
|
|
3
|
+
|
|
4
|
+
Event Bus simples para distribuição de ticks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TickBus:
|
|
13
|
+
"""
|
|
14
|
+
Implementa um Event Bus simples para ticks.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self.subscribers = []
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
def subscribe(self, handler):
|
|
23
|
+
"""
|
|
24
|
+
Registra um subscriber para receber ticks.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
self.subscribers.append(handler)
|
|
28
|
+
|
|
29
|
+
name = getattr(handler, "__qualname__", handler.__class__.__name__)
|
|
30
|
+
|
|
31
|
+
logger.debug("Subscriber registrado: %s", name)
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
def publish(self, tick):
|
|
36
|
+
"""
|
|
37
|
+
Publica um tick para todos os subscribers.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
for handler in self.subscribers:
|
|
41
|
+
try:
|
|
42
|
+
handler(tick)
|
|
43
|
+
|
|
44
|
+
except Exception:
|
|
45
|
+
logger.exception("Erro em subscriber")
|
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
Este cache é usado principalmente por:
|
|
8
|
+
|
|
9
|
+
- plugins em tempo real
|
|
10
|
+
- cálculos de indicadores
|
|
11
|
+
- geração de renko
|
|
12
|
+
- tape reading
|
|
13
|
+
- consultas recentes
|
|
14
|
+
|
|
15
|
+
A estrutura utiliza `collections.deque`, que oferece:
|
|
16
|
+
|
|
17
|
+
- inserção O(1)
|
|
18
|
+
- remoção automática quando atinge max_size
|
|
19
|
+
- excelente performance para streams de dados
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from collections import deque
|
|
23
|
+
from typing import Iterable, Iterator, Any, List
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TickCache:
|
|
27
|
+
"""
|
|
28
|
+
Mantém uma janela deslizante de ticks em memória.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
max_size : int
|
|
33
|
+
Número máximo de ticks armazenados no cache.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, max_size: int = 10000):
|
|
37
|
+
|
|
38
|
+
self.buffer: deque = deque(maxlen=max_size)
|
|
39
|
+
|
|
40
|
+
# ==========================================================
|
|
41
|
+
# INSERÇÃO
|
|
42
|
+
# ==========================================================
|
|
43
|
+
|
|
44
|
+
def add(self, tick: Any) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Adiciona um único tick ao cache.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
tick : Any
|
|
51
|
+
Tick a ser armazenado.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
self.buffer.append(tick)
|
|
55
|
+
|
|
56
|
+
# ----------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
def add_many(self, ticks: Iterable[Any]) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Adiciona múltiplos ticks ao cache.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
ticks : iterable
|
|
65
|
+
Coleção de ticks.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
for t in ticks:
|
|
69
|
+
self.buffer.append(t)
|
|
70
|
+
|
|
71
|
+
# ==========================================================
|
|
72
|
+
# CONSULTA
|
|
73
|
+
# ==========================================================
|
|
74
|
+
|
|
75
|
+
def get_last(self) -> Any:
|
|
76
|
+
"""
|
|
77
|
+
Retorna o último tick armazenado.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
tick ou None
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
if self.buffer:
|
|
85
|
+
return self.buffer[-1]
|
|
86
|
+
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
# ----------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
def get_last_n(self, n: int) -> List[Any]:
|
|
92
|
+
"""
|
|
93
|
+
Retorna os últimos N ticks do cache.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
n : int
|
|
98
|
+
Número de ticks desejados.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
list
|
|
103
|
+
Lista contendo os últimos ticks.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
if n <= 0:
|
|
107
|
+
return []
|
|
108
|
+
|
|
109
|
+
return list(self.buffer)[-n:]
|
|
110
|
+
|
|
111
|
+
# ----------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def get_all(self) -> List[Any]:
|
|
114
|
+
"""
|
|
115
|
+
Retorna todos os ticks armazenados no cache.
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
list
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
return list(self.buffer)
|
|
123
|
+
|
|
124
|
+
# ==========================================================
|
|
125
|
+
# UTILIDADES
|
|
126
|
+
# ==========================================================
|
|
127
|
+
|
|
128
|
+
def clear(self) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Limpa completamente o cache.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
self.buffer.clear()
|
|
134
|
+
|
|
135
|
+
# ----------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def __len__(self) -> int:
|
|
138
|
+
"""
|
|
139
|
+
Retorna o número atual de ticks armazenados.
|
|
140
|
+
|
|
141
|
+
Permite uso de:
|
|
142
|
+
|
|
143
|
+
len(cache)
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
return len(self.buffer)
|
|
147
|
+
|
|
148
|
+
# ----------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
def __iter__(self) -> Iterator[Any]:
|
|
151
|
+
"""
|
|
152
|
+
Permite iterar diretamente sobre o cache.
|
|
153
|
+
|
|
154
|
+
Exemplo:
|
|
155
|
+
|
|
156
|
+
for tick in cache:
|
|
157
|
+
...
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
return iter(self.buffer)
|