mtcli 3.8.0.dev17__tar.gz → 4.0.0.dev1__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.dev17 → mtcli-4.0.0.dev1}/PKG-INFO +1 -1
- mtcli-4.0.0.dev1/mtcli/cli.py +51 -0
- mtcli-4.0.0.dev1/mtcli/commands/bars.py +75 -0
- mtcli-4.0.0.dev1/mtcli/conf.py +125 -0
- mtcli-4.0.0.dev1/mtcli/controllers/bars_controller.py +75 -0
- mtcli-4.0.0.dev1/mtcli/data/base.py +20 -0
- mtcli-4.0.0.dev1/mtcli/data/csv.py +63 -0
- mtcli-4.0.0.dev1/mtcli/data/factory.py +29 -0
- mtcli-4.0.0.dev1/mtcli/data/mt5.py +148 -0
- mtcli-4.0.0.dev1/mtcli/domain/bar.py +40 -0
- mtcli-4.0.0.dev1/mtcli/domain/calculations.py +6 -0
- mtcli-4.0.0.dev1/mtcli/domain/config.py +4 -0
- mtcli-4.0.0.dev1/mtcli/domain/pattern.py +10 -0
- mtcli-4.0.0.dev1/mtcli/domain/price.py +13 -0
- mtcli-4.0.0.dev1/mtcli/domain/result.py +8 -0
- mtcli-4.0.0.dev1/mtcli/domain/session.py +11 -0
- mtcli-4.0.0.dev1/mtcli/domain/structure.py +7 -0
- mtcli-4.0.0.dev1/mtcli/domain/symbol.py +10 -0
- mtcli-4.0.0.dev1/mtcli/domain/tick.py +9 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/domain/timeframe.py +15 -0
- mtcli-4.0.0.dev1/mtcli/logger.py +136 -0
- mtcli-4.0.0.dev1/mtcli/models/bars_model.py +179 -0
- mtcli-4.0.0.dev1/mtcli/models/rate_model.py +71 -0
- mtcli-4.0.0.dev1/mtcli/models_legado/__init__.py +1 -0
- {mtcli-3.8.0.dev17/mtcli/models → mtcli-4.0.0.dev1/mtcli/models_legado}/bar_model.py +105 -105
- {mtcli-3.8.0.dev17/mtcli/models → mtcli-4.0.0.dev1/mtcli/models_legado}/bars_model.py +1 -1
- mtcli-4.0.0.dev1/mtcli/models_legado/chart.py +26 -0
- mtcli-4.0.0.dev1/mtcli/models_legado/consecutive.py +29 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/mt5_context.py +5 -1
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugin.py +18 -18
- mtcli-4.0.0.dev1/mtcli/plugins/media_movel/cli.py +73 -0
- mtcli-4.0.0.dev1/mtcli/plugins/media_movel/conf.py +75 -0
- mtcli-4.0.0.dev1/mtcli/plugins/media_movel/controller.py +43 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/media_movel/tests/test_mm.py +1 -1
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/media_movel/tests/test_model_media_movel.py +1 -1
- mtcli-4.0.0.dev1/mtcli/plugins/media_movel/view.py +15 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/range_medio/cli.py +2 -2
- mtcli-4.0.0.dev1/mtcli/plugins/range_medio/conf.py +67 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/range_medio/models/average_range_model.py +3 -3
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/volume_medio/cli.py +2 -2
- mtcli-4.0.0.dev1/mtcli/plugins/volume_medio/conf.py +67 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/volume_medio/models/model_average_volume.py +3 -3
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/utils/time.py +65 -65
- mtcli-4.0.0.dev1/mtcli/views/base_view.py +80 -0
- mtcli-4.0.0.dev1/mtcli/views/factory_view.py +30 -0
- mtcli-4.0.0.dev1/mtcli/views/full_view.py +40 -0
- mtcli-4.0.0.dev1/mtcli/views/hl_view.py +32 -0
- mtcli-4.0.0.dev1/mtcli/views/range_view.py +47 -0
- mtcli-4.0.0.dev1/mtcli/views/rate_view.py +18 -0
- mtcli-4.0.0.dev1/mtcli/views/volume_view.py +62 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/pyproject.toml +1 -1
- mtcli-3.8.0.dev17/mtcli/cli.py +0 -99
- mtcli-3.8.0.dev17/mtcli/cli_dev.py +0 -8
- mtcli-3.8.0.dev17/mtcli/commands/backfill.py +0 -70
- mtcli-3.8.0.dev17/mtcli/commands/bars.py +0 -147
- mtcli-3.8.0.dev17/mtcli/commands/ticks.py +0 -58
- mtcli-3.8.0.dev17/mtcli/commands_dev/migrate.py +0 -48
- mtcli-3.8.0.dev17/mtcli/conf.py +0 -331
- mtcli-3.8.0.dev17/mtcli/data/base.py +0 -9
- mtcli-3.8.0.dev17/mtcli/data/csv.py +0 -31
- mtcli-3.8.0.dev17/mtcli/data/mt5.py +0 -95
- mtcli-3.8.0.dev17/mtcli/database.py +0 -137
- mtcli-3.8.0.dev17/mtcli/logger.py +0 -178
- mtcli-3.8.0.dev17/mtcli/marketdata/backfill_engine.py +0 -136
- mtcli-3.8.0.dev17/mtcli/marketdata/tick_bus.py +0 -41
- mtcli-3.8.0.dev17/mtcli/marketdata/tick_cache.py +0 -160
- mtcli-3.8.0.dev17/mtcli/marketdata/tick_engine.py +0 -87
- mtcli-3.8.0.dev17/mtcli/marketdata/tick_repository.py +0 -333
- mtcli-3.8.0.dev17/mtcli/marketdata/tick_writer.py +0 -44
- mtcli-3.8.0.dev17/mtcli/marketdata/trade_tick_filter.py +0 -39
- mtcli-3.8.0.dev17/mtcli/migrations/001_initial_schema.py +0 -51
- mtcli-3.8.0.dev17/mtcli/migrations/002_ticks_time_msc.py +0 -53
- mtcli-3.8.0.dev17/mtcli/migrations/003_optimize_ticks_without_rowid.py +0 -86
- mtcli-3.8.0.dev17/mtcli/migrations/004_ticks_pk_time_msc.py +0 -78
- mtcli-3.8.0.dev17/mtcli/migrations/005_tick_price_compression.py +0 -86
- mtcli-3.8.0.dev17/mtcli/migrations/006_add_index_ticks_symbol_time.py +0 -32
- mtcli-3.8.0.dev17/mtcli/migrations/007_deduplicate_ticks_and_add_unique_index.py +0 -91
- mtcli-3.8.0.dev17/mtcli/migrations/008_fix_ticks_unique_index.py +0 -98
- mtcli-3.8.0.dev17/mtcli/migrations/009_keep_only_trade_ticks.py +0 -84
- mtcli-3.8.0.dev17/mtcli/migrations/__main__.py +0 -7
- mtcli-3.8.0.dev17/mtcli/migrations/runner.py +0 -301
- mtcli-3.8.0.dev17/mtcli/models/chart_model.py +0 -155
- mtcli-3.8.0.dev17/mtcli/models/conf_model.py +0 -32
- mtcli-3.8.0.dev17/mtcli/models/consecutive_bars_model.py +0 -77
- mtcli-3.8.0.dev17/mtcli/models/signals_model.py +0 -100
- mtcli-3.8.0.dev17/mtcli/models/unconsecutive_bar_model.py +0 -67
- mtcli-3.8.0.dev17/mtcli/plugins/media_movel/cli.py +0 -98
- mtcli-3.8.0.dev17/mtcli/plugins/media_movel/conf.py +0 -4
- mtcli-3.8.0.dev17/mtcli/plugins/media_movel/models/__init__.py +0 -1
- mtcli-3.8.0.dev17/mtcli/plugins/range_medio/conf.py +0 -1
- mtcli-3.8.0.dev17/mtcli/plugins/volume_medio/conf.py +0 -1
- mtcli-3.8.0.dev17/mtcli/plugins/volume_medio/tests/__init__.py +0 -0
- mtcli-3.8.0.dev17/mtcli/services/__init__.py +0 -0
- mtcli-3.8.0.dev17/mtcli/services/maintenance_service.py +0 -170
- mtcli-3.8.0.dev17/mtcli/services/tick_service.py +0 -56
- mtcli-3.8.0.dev17/mtcli/utils/__init__.py +0 -0
- mtcli-3.8.0.dev17/mtcli/views/close_view.py +0 -37
- mtcli-3.8.0.dev17/mtcli/views/full_view.py +0 -65
- mtcli-3.8.0.dev17/mtcli/views/high_view.py +0 -37
- mtcli-3.8.0.dev17/mtcli/views/low_view.py +0 -37
- mtcli-3.8.0.dev17/mtcli/views/min_view.py +0 -42
- mtcli-3.8.0.dev17/mtcli/views/open_view.py +0 -37
- mtcli-3.8.0.dev17/mtcli/views/ranges_view.py +0 -41
- mtcli-3.8.0.dev17/mtcli/views/rates_view.py +0 -41
- mtcli-3.8.0.dev17/mtcli/views/vars_view.py +0 -46
- mtcli-3.8.0.dev17/mtcli/views/volumes_view.py +0 -51
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/LICENSE +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/README.md +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/__init__.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/__main__.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/commands/__init__.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/commands/conf.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/commands/doctor.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/conecta.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/config_registre.py +0 -0
- {mtcli-3.8.0.dev17/mtcli/commands_dev → mtcli-4.0.0.dev1/mtcli/controllers}/__init__.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/data/__init__.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/domain/__init__.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/models/__init__.py +0 -0
- {mtcli-3.8.0.dev17/mtcli/models → mtcli-4.0.0.dev1/mtcli/models_legado}/rates_model.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugin_loader.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugin_manager.py +0 -0
- {mtcli-3.8.0.dev17/mtcli/marketdata → mtcli-4.0.0.dev1/mtcli/plugins}/__init__.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/exemplo.py-dist +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/media_movel/__init__.py +0 -0
- /mtcli-3.8.0.dev17/mtcli/plugins/media_movel/models/model_media_movel.py → /mtcli-4.0.0.dev1/mtcli/plugins/media_movel/model.py +0 -0
- {mtcli-3.8.0.dev17/mtcli/migrations → mtcli-4.0.0.dev1/mtcli/plugins/media_movel/tests}/__init__.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/range_medio/__init__.py +0 -0
- {mtcli-3.8.0.dev17/mtcli/plugins → mtcli-4.0.0.dev1/mtcli/plugins/range_medio/models}/__init__.py +0 -0
- {mtcli-3.8.0.dev17/mtcli/plugins/media_movel → mtcli-4.0.0.dev1/mtcli/plugins/range_medio}/tests/__init__.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/range_medio/tests/test_rm.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/volume_medio/__init__.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/volume_medio/models/__init__.py +0 -0
- {mtcli-3.8.0.dev17/mtcli/plugins/range_medio/models → mtcli-4.0.0.dev1/mtcli/plugins/volume_medio/tests}/__init__.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/volume_medio/tests/test_vm.py +0 -0
- {mtcli-3.8.0.dev17/mtcli/plugins/range_medio/tests → mtcli-4.0.0.dev1/mtcli/utils}/__init__.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/utils/pidfile.py +0 -0
- {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/views/__init__.py +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI principal do mtcli.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import click
|
|
7
|
+
from .plugin_loader import load_plugins
|
|
8
|
+
from .logger import setup_logger
|
|
9
|
+
|
|
10
|
+
from .commands.bars import bars
|
|
11
|
+
from .commands.doctor import doctor
|
|
12
|
+
|
|
13
|
+
logger = setup_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group(context_settings={"max_content_width": 120}, invoke_without_command=True)
|
|
17
|
+
@click.version_option(package_name="mtcli")
|
|
18
|
+
@click.pass_context
|
|
19
|
+
def mt(ctx):
|
|
20
|
+
"""
|
|
21
|
+
CLI principal do mtcli.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
if ctx.invoked_subcommand is None:
|
|
25
|
+
click.echo(ctx.get_help())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
mt.add_command(doctor, name="doctor")
|
|
29
|
+
mt.add_command(bars, name="bars")
|
|
30
|
+
|
|
31
|
+
loaded_plugins = load_plugins(mt)
|
|
32
|
+
logger.info("Plugins carregados: %s", loaded_plugins)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@mt.command(name="plugins")
|
|
36
|
+
def list_plugins():
|
|
37
|
+
"""
|
|
38
|
+
Lista os plugins carregados no mtcli.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
if not loaded_plugins:
|
|
42
|
+
click.echo("Nenhum plugin carregado.")
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
click.echo("Plugins carregados:\n")
|
|
46
|
+
for name in loaded_plugins:
|
|
47
|
+
click.echo(f" {name}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
mt()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comando CLI `bars`.
|
|
3
|
+
|
|
4
|
+
Responsável por:
|
|
5
|
+
- receber parâmetros via terminal (Click)
|
|
6
|
+
- instanciar a fonte de dados
|
|
7
|
+
- delegar execução ao BarsController
|
|
8
|
+
- imprimir saída no terminal
|
|
9
|
+
|
|
10
|
+
Este comando é a camada de entrada (CLI) no padrão MVC do mtcli.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
from ..controllers.bars_controller import BarsController
|
|
16
|
+
from ..conf import (
|
|
17
|
+
DATA_SOURCE_NAME,
|
|
18
|
+
SYMBOL,
|
|
19
|
+
TIMEFRAME,
|
|
20
|
+
MAX_BARS,
|
|
21
|
+
VIEW,
|
|
22
|
+
VOLUME_TYPE,
|
|
23
|
+
)
|
|
24
|
+
from ..data.factory import create_data_source
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@click.command()
|
|
28
|
+
@click.option("--symbol", "-s", default=SYMBOL, show_default=True, help="Código do ativo (ex: WINM26)")
|
|
29
|
+
@click.option("--timeframe", "-t", "period", default=TIMEFRAME, show_default=True, help="Timeframe (ex: M1, M5, D1)")
|
|
30
|
+
@click.option("--max-bars", "-mb", "count", default=MAX_BARS, show_default=True, help="Quantidade de barras")
|
|
31
|
+
@click.option("--view", "-v", default=VIEW, show_default=True, help="Formato da view (hl_view, full_view, etc)")
|
|
32
|
+
@click.option("--date", "-d", default=None, show_default=True, help="Filtrar pregão (YYYY-MM-DD)")
|
|
33
|
+
@click.option("--numerator", "-n", is_flag=True, show_default=True, help="Numerar barras")
|
|
34
|
+
@click.option("--show-date", "-sd", is_flag=True, show_default=True, help="Exibir data/hora")
|
|
35
|
+
@click.option("--volume-type", "-vt", "volume", default=VOLUME_TYPE, show_default=True, help="Tipo de volume (tick/real)")
|
|
36
|
+
@click.option("--data-source", "-ds", "source", default=None, show_default=True, help="Fonte de dados (mt5/csv)")
|
|
37
|
+
def bars(symbol, period, count, view, date, numerator, show_date, volume, source):
|
|
38
|
+
"""
|
|
39
|
+
Executa o comando `bars`.
|
|
40
|
+
|
|
41
|
+
Fluxo:
|
|
42
|
+
1. Resolve fonte de dados
|
|
43
|
+
2. Executa controller
|
|
44
|
+
3. Renderiza saída linha a linha
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
symbol (str): Ativo
|
|
48
|
+
period (str): Timeframe
|
|
49
|
+
count (int): Quantidade de barras
|
|
50
|
+
view (str): Nome da view
|
|
51
|
+
date (str | None): Filtro de pregão
|
|
52
|
+
numerator (bool): Numerar barras
|
|
53
|
+
show_date (bool): Mostrar timestamp
|
|
54
|
+
volume (str | None): Tipo de volume
|
|
55
|
+
source (str | None): Fonte de dados
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
source_name = source or DATA_SOURCE_NAME
|
|
59
|
+
data_source = create_data_source(source_name)
|
|
60
|
+
|
|
61
|
+
controller = BarsController(data_source)
|
|
62
|
+
|
|
63
|
+
lines = controller.execute(
|
|
64
|
+
symbol=symbol,
|
|
65
|
+
period=period,
|
|
66
|
+
count=count,
|
|
67
|
+
date=date,
|
|
68
|
+
view=view,
|
|
69
|
+
numerator=numerator,
|
|
70
|
+
show_date=show_date,
|
|
71
|
+
volume=volume,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
for line in lines:
|
|
75
|
+
click.echo(line)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sistema central de configuração do mtcli.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import configparser
|
|
7
|
+
import MetaTrader5 as mt5
|
|
8
|
+
|
|
9
|
+
from mtcli.mt5_context import mt5_conexao
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Config:
|
|
13
|
+
def __init__(self, filename="mtcli.ini"):
|
|
14
|
+
self.config = configparser.ConfigParser()
|
|
15
|
+
self.config.read(filename)
|
|
16
|
+
|
|
17
|
+
def get(self, key, section="DEFAULT", cast=None, default=None):
|
|
18
|
+
env_key = f"{section.upper()}_{key.upper()}"
|
|
19
|
+
value = os.getenv(env_key) or os.getenv(key.upper())
|
|
20
|
+
|
|
21
|
+
if value is None:
|
|
22
|
+
if self.config.has_option(section, key):
|
|
23
|
+
value = self.config.get(section, key)
|
|
24
|
+
elif self.config.has_option("DEFAULT", key):
|
|
25
|
+
value = self.config.get("DEFAULT", key)
|
|
26
|
+
else:
|
|
27
|
+
value = default
|
|
28
|
+
|
|
29
|
+
if cast and value is not None:
|
|
30
|
+
try:
|
|
31
|
+
if cast is bool:
|
|
32
|
+
value = str(value).lower() in ("1", "true", "yes")
|
|
33
|
+
else:
|
|
34
|
+
value = cast(value)
|
|
35
|
+
except ValueError:
|
|
36
|
+
value = default
|
|
37
|
+
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
def section(self, section):
|
|
41
|
+
class Section:
|
|
42
|
+
def __init__(self, parent, section):
|
|
43
|
+
self.parent = parent
|
|
44
|
+
self.section = section
|
|
45
|
+
|
|
46
|
+
def get(self, key, cast=None, default=None):
|
|
47
|
+
return self.parent.get(key, self.section, cast, default)
|
|
48
|
+
|
|
49
|
+
return Section(self, section)
|
|
50
|
+
|
|
51
|
+
def get_csv_path(self):
|
|
52
|
+
path = self.get("mt5_pasta")
|
|
53
|
+
|
|
54
|
+
if path:
|
|
55
|
+
return os.path.normpath(path) + os.sep
|
|
56
|
+
|
|
57
|
+
with mt5_conexao():
|
|
58
|
+
info = mt5.terminal_info()
|
|
59
|
+
|
|
60
|
+
if info is None:
|
|
61
|
+
raise RuntimeError(
|
|
62
|
+
"Não foi possível obter informações do terminal MT5."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
path = os.path.join(info.data_path, "MQL5", "Files")
|
|
66
|
+
return os.path.normpath(path) + os.sep
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# instância global
|
|
70
|
+
conf = Config()
|
|
71
|
+
|
|
72
|
+
# compatibilidade retroativa
|
|
73
|
+
config = conf.config
|
|
74
|
+
|
|
75
|
+
# ----------------------------
|
|
76
|
+
# CONFIGURAÇÕES
|
|
77
|
+
# ----------------------------
|
|
78
|
+
|
|
79
|
+
DATA_SOURCE_NAME = conf.get("dados", default="mt5").lower()
|
|
80
|
+
|
|
81
|
+
SYMBOL = conf.get("symbol", default="WIN$N")
|
|
82
|
+
DIGITS = conf.get("digits", cast=int, default=0)
|
|
83
|
+
TIMEFRAME = conf.get("timeframe", default="M5")
|
|
84
|
+
MAX_BARS = conf.get("max_bars", cast=int, default=20)
|
|
85
|
+
VIEW = conf.get("view", default="hl")
|
|
86
|
+
VOLUME_TYPE = conf.get("volume", default="tick")
|
|
87
|
+
DATE = conf.get("date", default="")
|
|
88
|
+
|
|
89
|
+
PERCENTUAL_BREAKOUT = conf.get("percentual_breakout", cast=int, default=50)
|
|
90
|
+
PERCENTUAL_DOJI = conf.get("percentual_doji", cast=int, default=10)
|
|
91
|
+
|
|
92
|
+
_INITIAL_CSV_PATH = conf.get_csv_path()
|
|
93
|
+
|
|
94
|
+
# ----------------------------
|
|
95
|
+
# LABELS
|
|
96
|
+
# ----------------------------
|
|
97
|
+
|
|
98
|
+
DOJI = conf.get("lateral", default="doji")
|
|
99
|
+
UP = conf.get("up", default="verde")
|
|
100
|
+
DOWN = conf.get("down", default="vermelho")
|
|
101
|
+
|
|
102
|
+
ASCENDING = conf.get("ascending", default="asc")
|
|
103
|
+
DESCENDING = conf.get("descending", default="desc")
|
|
104
|
+
INTERNAL = conf.get("internal", default="int")
|
|
105
|
+
EXTERNAL = conf.get("external", default="ext")
|
|
106
|
+
UNKNOW = conf.get("unknow", default="unk")
|
|
107
|
+
|
|
108
|
+
UPPER_WICK = conf.get("upper_wick", default="upper")
|
|
109
|
+
LOWER_WICK = conf.get("lower_wick", default="lower")
|
|
110
|
+
|
|
111
|
+
# ----------------------------
|
|
112
|
+
# PROCESSOS
|
|
113
|
+
# ----------------------------
|
|
114
|
+
|
|
115
|
+
RUN_DIR = os.path.join(
|
|
116
|
+
os.getenv("APPDATA", os.path.expanduser("~")),
|
|
117
|
+
"mtcli",
|
|
118
|
+
"run"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
os.makedirs(RUN_DIR, exist_ok=True)
|
|
122
|
+
|
|
123
|
+
PID_FILE = os.path.join(RUN_DIR, "risco.pid")
|
|
124
|
+
STOP_FILE = os.path.join(RUN_DIR, "risco.stop")
|
|
125
|
+
HEARTBEAT_FILE = os.path.join(RUN_DIR, "risco.heartbeat")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Controller do comando `bars`.
|
|
3
|
+
|
|
4
|
+
Orquestra o fluxo principal:
|
|
5
|
+
- coleta dados via DataSource
|
|
6
|
+
- converte para DTO
|
|
7
|
+
- transforma em BarsModel
|
|
8
|
+
- delega renderização para View
|
|
9
|
+
|
|
10
|
+
Não contém lógica de apresentação nem acesso direto à CLI.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from ..logger import setup_logger
|
|
14
|
+
from ..models.rate_model import RateDTO
|
|
15
|
+
from ..models.bars_model import BarsModel
|
|
16
|
+
from ..views.factory_view import ViewFactory
|
|
17
|
+
from ..data.base import DataSourceBase
|
|
18
|
+
|
|
19
|
+
log = setup_logger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BarsController:
|
|
23
|
+
"""
|
|
24
|
+
Controller responsável pela execução do comando `bars`.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, data_source: DataSourceBase):
|
|
28
|
+
"""
|
|
29
|
+
Args:
|
|
30
|
+
data_source (DataSourceBase): Fonte de dados (MT5, CSV, etc)
|
|
31
|
+
"""
|
|
32
|
+
self.data_source = data_source
|
|
33
|
+
|
|
34
|
+
def execute(
|
|
35
|
+
self,
|
|
36
|
+
symbol: str,
|
|
37
|
+
period: str,
|
|
38
|
+
count: int,
|
|
39
|
+
date: str | None,
|
|
40
|
+
view: str,
|
|
41
|
+
numerator: bool,
|
|
42
|
+
show_date: bool,
|
|
43
|
+
volume: str | None,
|
|
44
|
+
) -> list[str]:
|
|
45
|
+
"""
|
|
46
|
+
Executa o fluxo completo do comando.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
list[str]: Linhas prontas para impressão
|
|
50
|
+
"""
|
|
51
|
+
log.info(
|
|
52
|
+
"bars | symbol=%s period=%s count=%s view=%s date=%s",
|
|
53
|
+
symbol,
|
|
54
|
+
period,
|
|
55
|
+
count,
|
|
56
|
+
view,
|
|
57
|
+
date,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
raw_rates = self.data_source.get_data(symbol, period, count)
|
|
61
|
+
|
|
62
|
+
rates = [RateDTO.from_list(rate) for rate in raw_rates]
|
|
63
|
+
|
|
64
|
+
bars = BarsModel(rates, date_filter=date).build()
|
|
65
|
+
|
|
66
|
+
view_instance = ViewFactory.create(
|
|
67
|
+
name=view,
|
|
68
|
+
bars=bars,
|
|
69
|
+
period=period,
|
|
70
|
+
numerator=numerator,
|
|
71
|
+
show_date=show_date,
|
|
72
|
+
volume=volume,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return view_instance.render()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Módulo da classe base para coleta de dados.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DataSourceBase:
|
|
9
|
+
"""
|
|
10
|
+
Interface base para fontes de dados.
|
|
11
|
+
|
|
12
|
+
Todas as implementações devem retornar:
|
|
13
|
+
|
|
14
|
+
List[
|
|
15
|
+
[timestamp, open, high, low, close, tick_volume, real_volume]
|
|
16
|
+
]
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def get_data(self, symbol: str, period: str, count: int = 100) -> List[list]:
|
|
20
|
+
raise NotImplementedError("O método get_data deve ser implementado.")
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Módulo fonte de dados via CSV.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
from mtcli.conf import conf
|
|
9
|
+
from mtcli.logger import setup_logger
|
|
10
|
+
from .base import DataSourceBase
|
|
11
|
+
|
|
12
|
+
logger = setup_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CsvDataSource(DataSourceBase):
|
|
16
|
+
"""Fonte de dados via CSV."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, base_path: str | None = None):
|
|
19
|
+
"""
|
|
20
|
+
Args:
|
|
21
|
+
base_path: caminho opcional para sobrescrever pasta padrão
|
|
22
|
+
"""
|
|
23
|
+
self.base_path = base_path or conf.get_csv_path()
|
|
24
|
+
|
|
25
|
+
def get_data(self, symbol, period, count=100):
|
|
26
|
+
"""
|
|
27
|
+
Retorna dados CSV em formato padrão mtcli.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
symbol (str)
|
|
31
|
+
period (str)
|
|
32
|
+
count (int)
|
|
33
|
+
"""
|
|
34
|
+
file_path = os.path.join(self.base_path, f"{symbol}{period}.csv")
|
|
35
|
+
|
|
36
|
+
logger.info("CSV | lendo arquivo: %s", file_path)
|
|
37
|
+
|
|
38
|
+
csv_data = []
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
with open(file_path, encoding="utf-16", newline="") as f:
|
|
42
|
+
reader = csv.reader(f, delimiter=",", quotechar="'")
|
|
43
|
+
|
|
44
|
+
for row in reader:
|
|
45
|
+
if not row:
|
|
46
|
+
continue
|
|
47
|
+
csv_data.append(row)
|
|
48
|
+
|
|
49
|
+
except FileNotFoundError:
|
|
50
|
+
logger.warning("Arquivo não encontrado: %s", file_path)
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.exception("Erro ao ler CSV: %s", file_path)
|
|
55
|
+
raise e
|
|
56
|
+
|
|
57
|
+
# mantém apenas as últimas N barras (igual MT5)
|
|
58
|
+
if count:
|
|
59
|
+
csv_data = csv_data[-count:]
|
|
60
|
+
|
|
61
|
+
logger.info("CSV | %s registros carregados", len(csv_data))
|
|
62
|
+
|
|
63
|
+
return csv_data
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Factory de DataSources.
|
|
3
|
+
|
|
4
|
+
Responsável por instanciar fontes de dados sem acoplamento
|
|
5
|
+
e evitando import circular.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_data_source(name: str):
|
|
10
|
+
"""
|
|
11
|
+
Cria uma fonte de dados a partir do nome.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
name (str): "mt5" ou "csv"
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
DataSourceBase
|
|
18
|
+
"""
|
|
19
|
+
name = (name or "mt5").lower()
|
|
20
|
+
|
|
21
|
+
if name == "mt5":
|
|
22
|
+
from mtcli.data.mt5 import MT5DataSource
|
|
23
|
+
return MT5DataSource()
|
|
24
|
+
|
|
25
|
+
if name == "csv":
|
|
26
|
+
from mtcli.data.csv import CsvDataSource
|
|
27
|
+
return CsvDataSource()
|
|
28
|
+
|
|
29
|
+
raise ValueError(f"Fonte de dados desconhecida: {name}")
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Módulo fonte de dados via API do MetaTrader 5.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
import MetaTrader5 as mt5
|
|
8
|
+
|
|
9
|
+
from mtcli.logger import setup_logger
|
|
10
|
+
from mtcli.mt5_context import mt5_conexao
|
|
11
|
+
from .base import DataSourceBase
|
|
12
|
+
|
|
13
|
+
log = setup_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------
|
|
17
|
+
# Timeframes suportados (constante global)
|
|
18
|
+
# ---------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
TF_MAP = {
|
|
21
|
+
"M1": mt5.TIMEFRAME_M1,
|
|
22
|
+
"M2": mt5.TIMEFRAME_M2,
|
|
23
|
+
"M3": mt5.TIMEFRAME_M3,
|
|
24
|
+
"M4": mt5.TIMEFRAME_M4,
|
|
25
|
+
"M5": mt5.TIMEFRAME_M5,
|
|
26
|
+
"M6": mt5.TIMEFRAME_M6,
|
|
27
|
+
"M10": mt5.TIMEFRAME_M10,
|
|
28
|
+
"M12": mt5.TIMEFRAME_M12,
|
|
29
|
+
"M15": mt5.TIMEFRAME_M15,
|
|
30
|
+
"M20": mt5.TIMEFRAME_M20,
|
|
31
|
+
"M30": mt5.TIMEFRAME_M30,
|
|
32
|
+
"H1": mt5.TIMEFRAME_H1,
|
|
33
|
+
"H2": mt5.TIMEFRAME_H2,
|
|
34
|
+
"H3": mt5.TIMEFRAME_H3,
|
|
35
|
+
"H4": mt5.TIMEFRAME_H4,
|
|
36
|
+
"H6": mt5.TIMEFRAME_H6,
|
|
37
|
+
"H8": mt5.TIMEFRAME_H8,
|
|
38
|
+
"H12": mt5.TIMEFRAME_H12,
|
|
39
|
+
"D1": mt5.TIMEFRAME_D1,
|
|
40
|
+
"W1": mt5.TIMEFRAME_W1,
|
|
41
|
+
"MN1": mt5.TIMEFRAME_MN1,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------
|
|
46
|
+
# DataSource
|
|
47
|
+
# ---------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
class MT5DataSource(DataSourceBase):
|
|
50
|
+
"""Fonte de dados via API do MetaTrader 5."""
|
|
51
|
+
|
|
52
|
+
CORRETORAS_B3 = (
|
|
53
|
+
"clear",
|
|
54
|
+
"xp",
|
|
55
|
+
"rico",
|
|
56
|
+
"modal",
|
|
57
|
+
"terra",
|
|
58
|
+
"btg",
|
|
59
|
+
"toro",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def _normalize_symbol(self, symbol: str) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Normaliza símbolo dependendo da corretora.
|
|
65
|
+
"""
|
|
66
|
+
info = mt5.account_info()
|
|
67
|
+
|
|
68
|
+
if info is None:
|
|
69
|
+
log.warning("Não foi possível obter account_info do MT5.")
|
|
70
|
+
return symbol
|
|
71
|
+
|
|
72
|
+
company = (info.company or "").lower()
|
|
73
|
+
|
|
74
|
+
if any(c in company for c in self.CORRETORAS_B3):
|
|
75
|
+
return symbol.upper()
|
|
76
|
+
|
|
77
|
+
return symbol
|
|
78
|
+
|
|
79
|
+
def _convert_time(self, timestamp) -> str:
|
|
80
|
+
"""
|
|
81
|
+
Converte timestamp do MT5 para string padrão mtcli.
|
|
82
|
+
"""
|
|
83
|
+
# MT5 geralmente retorna epoch (int)
|
|
84
|
+
if isinstance(timestamp, (int, float)):
|
|
85
|
+
dt = datetime.fromtimestamp(timestamp)
|
|
86
|
+
else:
|
|
87
|
+
# fallback seguro
|
|
88
|
+
dt = datetime.fromtimestamp(int(timestamp))
|
|
89
|
+
|
|
90
|
+
return dt.strftime("%Y.%m.%d %H:%M:%S")
|
|
91
|
+
|
|
92
|
+
def get_data(self, symbol, period, count=100):
|
|
93
|
+
"""
|
|
94
|
+
Retorna uma lista de listas no formato padrão mtcli.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
period = period.upper()
|
|
98
|
+
|
|
99
|
+
if period not in TF_MAP:
|
|
100
|
+
log.error("Timeframe inválido: %s", period)
|
|
101
|
+
raise ValueError(f"Timeframe '{period}' inválido.")
|
|
102
|
+
|
|
103
|
+
log.info(
|
|
104
|
+
"MT5 | coleta iniciada | symbol=%s period=%s count=%s",
|
|
105
|
+
symbol,
|
|
106
|
+
period,
|
|
107
|
+
count,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
with mt5_conexao():
|
|
111
|
+
symbol_normalized = self._normalize_symbol(symbol)
|
|
112
|
+
|
|
113
|
+
log.info("MT5 | símbolo normalizado: %s", symbol_normalized)
|
|
114
|
+
|
|
115
|
+
rates = mt5.copy_rates_from_pos(
|
|
116
|
+
symbol_normalized,
|
|
117
|
+
TF_MAP[period],
|
|
118
|
+
0,
|
|
119
|
+
count,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if rates is None:
|
|
123
|
+
error = mt5.last_error()
|
|
124
|
+
log.error("MT5 | erro ao obter dados: %s", error)
|
|
125
|
+
raise RuntimeError(f"Erro MT5: {error}")
|
|
126
|
+
|
|
127
|
+
result = []
|
|
128
|
+
|
|
129
|
+
for r in rates:
|
|
130
|
+
try:
|
|
131
|
+
result.append(
|
|
132
|
+
[
|
|
133
|
+
self._convert_time(r["time"]),
|
|
134
|
+
float(r["open"]),
|
|
135
|
+
float(r["high"]),
|
|
136
|
+
float(r["low"]),
|
|
137
|
+
float(r["close"]),
|
|
138
|
+
int(r["tick_volume"]) if r["tick_volume"] is not None else None,
|
|
139
|
+
int(r["real_volume"]) if r["real_volume"] is not None else None,
|
|
140
|
+
]
|
|
141
|
+
)
|
|
142
|
+
except Exception:
|
|
143
|
+
log.exception("Erro ao converter rate: %s", r)
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
log.info("MT5 | coleta finalizada | %s registros", len(result))
|
|
147
|
+
|
|
148
|
+
return result
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class Bar:
|
|
7
|
+
"""
|
|
8
|
+
Representa um candle OHLCV independente de fonte de dados.
|
|
9
|
+
|
|
10
|
+
- tick_volume: quantidade de ticks (atividade)
|
|
11
|
+
- real_volume: volume negociado (quando disponível)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
time: datetime
|
|
15
|
+
open: float
|
|
16
|
+
high: float
|
|
17
|
+
low: float
|
|
18
|
+
close: float
|
|
19
|
+
tick_volume: float
|
|
20
|
+
real_volume: float | None = None
|
|
21
|
+
|
|
22
|
+
def is_bull(self) -> bool:
|
|
23
|
+
return self.close > self.open
|
|
24
|
+
|
|
25
|
+
def is_bear(self) -> bool:
|
|
26
|
+
return self.close < self.open
|
|
27
|
+
|
|
28
|
+
def body(self) -> float:
|
|
29
|
+
return abs(self.close - self.open)
|
|
30
|
+
|
|
31
|
+
def range(self) -> float:
|
|
32
|
+
return self.high - self.low
|
|
33
|
+
|
|
34
|
+
def volume(self) -> float:
|
|
35
|
+
"""
|
|
36
|
+
Volume preferencial:
|
|
37
|
+
- usa real_volume se disponível
|
|
38
|
+
- fallback para tick_volume
|
|
39
|
+
"""
|
|
40
|
+
return self.real_volume or self.tick_volume
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True)
|
|
5
|
+
class Price:
|
|
6
|
+
value: float
|
|
7
|
+
digits: int
|
|
8
|
+
|
|
9
|
+
def normalize(self) -> float:
|
|
10
|
+
return round(self.value, self.digits)
|
|
11
|
+
|
|
12
|
+
def points(self, other: "Price") -> float:
|
|
13
|
+
return (self.value - other.value) * (10 ** self.digits)
|