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.
Files changed (138) hide show
  1. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/PKG-INFO +1 -1
  2. mtcli-4.0.0.dev1/mtcli/cli.py +51 -0
  3. mtcli-4.0.0.dev1/mtcli/commands/bars.py +75 -0
  4. mtcli-4.0.0.dev1/mtcli/conf.py +125 -0
  5. mtcli-4.0.0.dev1/mtcli/controllers/bars_controller.py +75 -0
  6. mtcli-4.0.0.dev1/mtcli/data/base.py +20 -0
  7. mtcli-4.0.0.dev1/mtcli/data/csv.py +63 -0
  8. mtcli-4.0.0.dev1/mtcli/data/factory.py +29 -0
  9. mtcli-4.0.0.dev1/mtcli/data/mt5.py +148 -0
  10. mtcli-4.0.0.dev1/mtcli/domain/bar.py +40 -0
  11. mtcli-4.0.0.dev1/mtcli/domain/calculations.py +6 -0
  12. mtcli-4.0.0.dev1/mtcli/domain/config.py +4 -0
  13. mtcli-4.0.0.dev1/mtcli/domain/pattern.py +10 -0
  14. mtcli-4.0.0.dev1/mtcli/domain/price.py +13 -0
  15. mtcli-4.0.0.dev1/mtcli/domain/result.py +8 -0
  16. mtcli-4.0.0.dev1/mtcli/domain/session.py +11 -0
  17. mtcli-4.0.0.dev1/mtcli/domain/structure.py +7 -0
  18. mtcli-4.0.0.dev1/mtcli/domain/symbol.py +10 -0
  19. mtcli-4.0.0.dev1/mtcli/domain/tick.py +9 -0
  20. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/domain/timeframe.py +15 -0
  21. mtcli-4.0.0.dev1/mtcli/logger.py +136 -0
  22. mtcli-4.0.0.dev1/mtcli/models/bars_model.py +179 -0
  23. mtcli-4.0.0.dev1/mtcli/models/rate_model.py +71 -0
  24. mtcli-4.0.0.dev1/mtcli/models_legado/__init__.py +1 -0
  25. {mtcli-3.8.0.dev17/mtcli/models → mtcli-4.0.0.dev1/mtcli/models_legado}/bar_model.py +105 -105
  26. {mtcli-3.8.0.dev17/mtcli/models → mtcli-4.0.0.dev1/mtcli/models_legado}/bars_model.py +1 -1
  27. mtcli-4.0.0.dev1/mtcli/models_legado/chart.py +26 -0
  28. mtcli-4.0.0.dev1/mtcli/models_legado/consecutive.py +29 -0
  29. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/mt5_context.py +5 -1
  30. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugin.py +18 -18
  31. mtcli-4.0.0.dev1/mtcli/plugins/media_movel/cli.py +73 -0
  32. mtcli-4.0.0.dev1/mtcli/plugins/media_movel/conf.py +75 -0
  33. mtcli-4.0.0.dev1/mtcli/plugins/media_movel/controller.py +43 -0
  34. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/media_movel/tests/test_mm.py +1 -1
  35. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/media_movel/tests/test_model_media_movel.py +1 -1
  36. mtcli-4.0.0.dev1/mtcli/plugins/media_movel/view.py +15 -0
  37. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/range_medio/cli.py +2 -2
  38. mtcli-4.0.0.dev1/mtcli/plugins/range_medio/conf.py +67 -0
  39. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/range_medio/models/average_range_model.py +3 -3
  40. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/volume_medio/cli.py +2 -2
  41. mtcli-4.0.0.dev1/mtcli/plugins/volume_medio/conf.py +67 -0
  42. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/volume_medio/models/model_average_volume.py +3 -3
  43. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/utils/time.py +65 -65
  44. mtcli-4.0.0.dev1/mtcli/views/base_view.py +80 -0
  45. mtcli-4.0.0.dev1/mtcli/views/factory_view.py +30 -0
  46. mtcli-4.0.0.dev1/mtcli/views/full_view.py +40 -0
  47. mtcli-4.0.0.dev1/mtcli/views/hl_view.py +32 -0
  48. mtcli-4.0.0.dev1/mtcli/views/range_view.py +47 -0
  49. mtcli-4.0.0.dev1/mtcli/views/rate_view.py +18 -0
  50. mtcli-4.0.0.dev1/mtcli/views/volume_view.py +62 -0
  51. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/pyproject.toml +1 -1
  52. mtcli-3.8.0.dev17/mtcli/cli.py +0 -99
  53. mtcli-3.8.0.dev17/mtcli/cli_dev.py +0 -8
  54. mtcli-3.8.0.dev17/mtcli/commands/backfill.py +0 -70
  55. mtcli-3.8.0.dev17/mtcli/commands/bars.py +0 -147
  56. mtcli-3.8.0.dev17/mtcli/commands/ticks.py +0 -58
  57. mtcli-3.8.0.dev17/mtcli/commands_dev/migrate.py +0 -48
  58. mtcli-3.8.0.dev17/mtcli/conf.py +0 -331
  59. mtcli-3.8.0.dev17/mtcli/data/base.py +0 -9
  60. mtcli-3.8.0.dev17/mtcli/data/csv.py +0 -31
  61. mtcli-3.8.0.dev17/mtcli/data/mt5.py +0 -95
  62. mtcli-3.8.0.dev17/mtcli/database.py +0 -137
  63. mtcli-3.8.0.dev17/mtcli/logger.py +0 -178
  64. mtcli-3.8.0.dev17/mtcli/marketdata/backfill_engine.py +0 -136
  65. mtcli-3.8.0.dev17/mtcli/marketdata/tick_bus.py +0 -41
  66. mtcli-3.8.0.dev17/mtcli/marketdata/tick_cache.py +0 -160
  67. mtcli-3.8.0.dev17/mtcli/marketdata/tick_engine.py +0 -87
  68. mtcli-3.8.0.dev17/mtcli/marketdata/tick_repository.py +0 -333
  69. mtcli-3.8.0.dev17/mtcli/marketdata/tick_writer.py +0 -44
  70. mtcli-3.8.0.dev17/mtcli/marketdata/trade_tick_filter.py +0 -39
  71. mtcli-3.8.0.dev17/mtcli/migrations/001_initial_schema.py +0 -51
  72. mtcli-3.8.0.dev17/mtcli/migrations/002_ticks_time_msc.py +0 -53
  73. mtcli-3.8.0.dev17/mtcli/migrations/003_optimize_ticks_without_rowid.py +0 -86
  74. mtcli-3.8.0.dev17/mtcli/migrations/004_ticks_pk_time_msc.py +0 -78
  75. mtcli-3.8.0.dev17/mtcli/migrations/005_tick_price_compression.py +0 -86
  76. mtcli-3.8.0.dev17/mtcli/migrations/006_add_index_ticks_symbol_time.py +0 -32
  77. mtcli-3.8.0.dev17/mtcli/migrations/007_deduplicate_ticks_and_add_unique_index.py +0 -91
  78. mtcli-3.8.0.dev17/mtcli/migrations/008_fix_ticks_unique_index.py +0 -98
  79. mtcli-3.8.0.dev17/mtcli/migrations/009_keep_only_trade_ticks.py +0 -84
  80. mtcli-3.8.0.dev17/mtcli/migrations/__main__.py +0 -7
  81. mtcli-3.8.0.dev17/mtcli/migrations/runner.py +0 -301
  82. mtcli-3.8.0.dev17/mtcli/models/chart_model.py +0 -155
  83. mtcli-3.8.0.dev17/mtcli/models/conf_model.py +0 -32
  84. mtcli-3.8.0.dev17/mtcli/models/consecutive_bars_model.py +0 -77
  85. mtcli-3.8.0.dev17/mtcli/models/signals_model.py +0 -100
  86. mtcli-3.8.0.dev17/mtcli/models/unconsecutive_bar_model.py +0 -67
  87. mtcli-3.8.0.dev17/mtcli/plugins/media_movel/cli.py +0 -98
  88. mtcli-3.8.0.dev17/mtcli/plugins/media_movel/conf.py +0 -4
  89. mtcli-3.8.0.dev17/mtcli/plugins/media_movel/models/__init__.py +0 -1
  90. mtcli-3.8.0.dev17/mtcli/plugins/range_medio/conf.py +0 -1
  91. mtcli-3.8.0.dev17/mtcli/plugins/volume_medio/conf.py +0 -1
  92. mtcli-3.8.0.dev17/mtcli/plugins/volume_medio/tests/__init__.py +0 -0
  93. mtcli-3.8.0.dev17/mtcli/services/__init__.py +0 -0
  94. mtcli-3.8.0.dev17/mtcli/services/maintenance_service.py +0 -170
  95. mtcli-3.8.0.dev17/mtcli/services/tick_service.py +0 -56
  96. mtcli-3.8.0.dev17/mtcli/utils/__init__.py +0 -0
  97. mtcli-3.8.0.dev17/mtcli/views/close_view.py +0 -37
  98. mtcli-3.8.0.dev17/mtcli/views/full_view.py +0 -65
  99. mtcli-3.8.0.dev17/mtcli/views/high_view.py +0 -37
  100. mtcli-3.8.0.dev17/mtcli/views/low_view.py +0 -37
  101. mtcli-3.8.0.dev17/mtcli/views/min_view.py +0 -42
  102. mtcli-3.8.0.dev17/mtcli/views/open_view.py +0 -37
  103. mtcli-3.8.0.dev17/mtcli/views/ranges_view.py +0 -41
  104. mtcli-3.8.0.dev17/mtcli/views/rates_view.py +0 -41
  105. mtcli-3.8.0.dev17/mtcli/views/vars_view.py +0 -46
  106. mtcli-3.8.0.dev17/mtcli/views/volumes_view.py +0 -51
  107. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/LICENSE +0 -0
  108. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/README.md +0 -0
  109. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/__init__.py +0 -0
  110. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/__main__.py +0 -0
  111. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/commands/__init__.py +0 -0
  112. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/commands/conf.py +0 -0
  113. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/commands/doctor.py +0 -0
  114. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/conecta.py +0 -0
  115. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/config_registre.py +0 -0
  116. {mtcli-3.8.0.dev17/mtcli/commands_dev → mtcli-4.0.0.dev1/mtcli/controllers}/__init__.py +0 -0
  117. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/data/__init__.py +0 -0
  118. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/domain/__init__.py +0 -0
  119. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/models/__init__.py +0 -0
  120. {mtcli-3.8.0.dev17/mtcli/models → mtcli-4.0.0.dev1/mtcli/models_legado}/rates_model.py +0 -0
  121. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugin_loader.py +0 -0
  122. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugin_manager.py +0 -0
  123. {mtcli-3.8.0.dev17/mtcli/marketdata → mtcli-4.0.0.dev1/mtcli/plugins}/__init__.py +0 -0
  124. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/exemplo.py-dist +0 -0
  125. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/media_movel/__init__.py +0 -0
  126. /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
  127. {mtcli-3.8.0.dev17/mtcli/migrations → mtcli-4.0.0.dev1/mtcli/plugins/media_movel/tests}/__init__.py +0 -0
  128. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/range_medio/__init__.py +0 -0
  129. {mtcli-3.8.0.dev17/mtcli/plugins → mtcli-4.0.0.dev1/mtcli/plugins/range_medio/models}/__init__.py +0 -0
  130. {mtcli-3.8.0.dev17/mtcli/plugins/media_movel → mtcli-4.0.0.dev1/mtcli/plugins/range_medio}/tests/__init__.py +0 -0
  131. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/range_medio/tests/test_rm.py +0 -0
  132. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/volume_medio/__init__.py +0 -0
  133. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/volume_medio/models/__init__.py +0 -0
  134. {mtcli-3.8.0.dev17/mtcli/plugins/range_medio/models → mtcli-4.0.0.dev1/mtcli/plugins/volume_medio/tests}/__init__.py +0 -0
  135. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/plugins/volume_medio/tests/test_vm.py +0 -0
  136. {mtcli-3.8.0.dev17/mtcli/plugins/range_medio/tests → mtcli-4.0.0.dev1/mtcli/utils}/__init__.py +0 -0
  137. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/utils/pidfile.py +0 -0
  138. {mtcli-3.8.0.dev17 → mtcli-4.0.0.dev1}/mtcli/views/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mtcli
3
- Version: 3.8.0.dev17
3
+ Version: 4.0.0.dev1
4
4
  Summary: Aplicativo CLI para exibir gráficos do MetaTrader 5 screen reader friendly
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -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,6 @@
1
+ def body_size(bar):
2
+ return abs(bar.close - bar.open)
3
+
4
+
5
+ def is_inside_bar(prev, curr):
6
+ return curr.high <= prev.high and curr.low >= prev.low
@@ -0,0 +1,4 @@
1
+ @dataclass(frozen=True)
2
+ class TradingConfig:
3
+ risk_per_trade: float
4
+ max_positions: int
@@ -0,0 +1,10 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Pattern(Enum):
5
+ H1 = "H1"
6
+ H2 = "H2"
7
+ L1 = "L1"
8
+ L2 = "L2"
9
+ BRF = "BRF"
10
+ BLF = "BLF"
@@ -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)
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Result:
6
+ success: bool
7
+ data: any = None
8
+ error: str | None = None