mtcli 3.7.2__tar.gz → 3.7.3__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 (78) hide show
  1. {mtcli-3.7.2 → mtcli-3.7.3}/PKG-INFO +2 -1
  2. mtcli-3.7.3/mtcli/__main__.py +3 -0
  3. mtcli-3.7.3/mtcli/cli.py +51 -0
  4. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/conf.py +30 -1
  5. mtcli-3.7.3/mtcli/logger.py +136 -0
  6. mtcli-3.7.3/mtcli/utils/time.py +65 -0
  7. {mtcli-3.7.2 → mtcli-3.7.3}/pyproject.toml +3 -1
  8. mtcli-3.7.2/mtcli/cli.py +0 -74
  9. mtcli-3.7.2/mtcli/database.py +0 -54
  10. mtcli-3.7.2/mtcli/logger.py +0 -178
  11. mtcli-3.7.2/mtcli/marketdata/tick_cache.py +0 -24
  12. mtcli-3.7.2/mtcli/marketdata/tick_repository.py +0 -142
  13. mtcli-3.7.2/mtcli/utils/__init__.py +0 -0
  14. {mtcli-3.7.2 → mtcli-3.7.3}/LICENSE +0 -0
  15. {mtcli-3.7.2 → mtcli-3.7.3}/README.md +0 -0
  16. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/__init__.py +0 -0
  17. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/commands/__init__.py +0 -0
  18. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/commands/bars.py +0 -0
  19. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/commands/conf.py +0 -0
  20. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/commands/doctor.py +0 -0
  21. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/conecta.py +0 -0
  22. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/config_registre.py +0 -0
  23. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/data/__init__.py +0 -0
  24. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/data/base.py +0 -0
  25. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/data/csv.py +0 -0
  26. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/data/mt5.py +0 -0
  27. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/domain/__init__.py +0 -0
  28. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/domain/timeframe.py +0 -0
  29. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/models/__init__.py +0 -0
  30. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/models/bar_model.py +0 -0
  31. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/models/bars_model.py +0 -0
  32. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/models/chart_model.py +0 -0
  33. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/models/conf_model.py +0 -0
  34. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/models/consecutive_bars_model.py +0 -0
  35. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/models/rates_model.py +0 -0
  36. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/models/signals_model.py +0 -0
  37. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/models/unconsecutive_bar_model.py +0 -0
  38. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/mt5_context.py +0 -0
  39. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugin.py +0 -0
  40. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugin_loader.py +0 -0
  41. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugin_manager.py +0 -0
  42. {mtcli-3.7.2/mtcli/marketdata → mtcli-3.7.3/mtcli/plugins}/__init__.py +0 -0
  43. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/exemplo.py-dist +0 -0
  44. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/media_movel/__init__.py +0 -0
  45. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/media_movel/cli.py +0 -0
  46. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/media_movel/conf.py +0 -0
  47. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/media_movel/models/__init__.py +0 -0
  48. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/media_movel/models/model_media_movel.py +0 -0
  49. {mtcli-3.7.2/mtcli/plugins → mtcli-3.7.3/mtcli/plugins/media_movel/tests}/__init__.py +0 -0
  50. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/media_movel/tests/test_mm.py +0 -0
  51. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/media_movel/tests/test_model_media_movel.py +0 -0
  52. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/range_medio/__init__.py +0 -0
  53. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/range_medio/cli.py +0 -0
  54. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/range_medio/conf.py +0 -0
  55. {mtcli-3.7.2/mtcli/plugins/media_movel/tests → mtcli-3.7.3/mtcli/plugins/range_medio/models}/__init__.py +0 -0
  56. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/range_medio/models/average_range_model.py +0 -0
  57. {mtcli-3.7.2/mtcli/plugins/range_medio/models → mtcli-3.7.3/mtcli/plugins/range_medio/tests}/__init__.py +0 -0
  58. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/range_medio/tests/test_rm.py +0 -0
  59. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/volume_medio/__init__.py +0 -0
  60. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/volume_medio/cli.py +0 -0
  61. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/volume_medio/conf.py +0 -0
  62. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/volume_medio/models/__init__.py +0 -0
  63. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/volume_medio/models/model_average_volume.py +0 -0
  64. {mtcli-3.7.2/mtcli/plugins/range_medio → mtcli-3.7.3/mtcli/plugins/volume_medio}/tests/__init__.py +0 -0
  65. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/plugins/volume_medio/tests/test_vm.py +0 -0
  66. {mtcli-3.7.2/mtcli/plugins/volume_medio/tests → mtcli-3.7.3/mtcli/utils}/__init__.py +0 -0
  67. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/utils/pidfile.py +0 -0
  68. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/views/__init__.py +0 -0
  69. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/views/close_view.py +0 -0
  70. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/views/full_view.py +0 -0
  71. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/views/high_view.py +0 -0
  72. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/views/low_view.py +0 -0
  73. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/views/min_view.py +0 -0
  74. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/views/open_view.py +0 -0
  75. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/views/ranges_view.py +0 -0
  76. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/views/rates_view.py +0 -0
  77. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/views/vars_view.py +0 -0
  78. {mtcli-3.7.2 → mtcli-3.7.3}/mtcli/views/volumes_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mtcli
3
- Version: 3.7.2
3
+ Version: 3.7.3
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
@@ -19,6 +19,7 @@ Classifier: Operating System :: OS Independent
19
19
  Classifier: Topic :: Office/Business :: Financial :: Investment
20
20
  Requires-Dist: click (>=8.2.1,<9.0.0)
21
21
  Requires-Dist: metatrader5 (>=5.0.5260,<6.0.0)
22
+ Requires-Dist: tzdata (>=2025.3,<2026.0)
22
23
  Project-URL: Documentation, https://mtcli.readthedocs.io/pt-br/latest
23
24
  Project-URL: Homepage, https://github.com:vfranca/mtcli
24
25
  Project-URL: Repository, https://github.com/vfranca/mtcli
@@ -0,0 +1,3 @@
1
+ from .cli import mt
2
+
3
+ mt()
@@ -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()
@@ -309,8 +309,37 @@ DATA_SOURCE = conf.get_data_source()
309
309
  _INITIAL_CSV_PATH = conf.get_csv_path()
310
310
 
311
311
  # ---------------------------------------------------------
312
- # controle de processo em execução
312
+ # Gestão de processos (daemon / serviços mtcli)
313
313
  # ---------------------------------------------------------
314
+ #
315
+ # Define caminhos para arquivos de controle utilizados por
316
+ # processos em background (ex: ticks, risco, etc.).
317
+ #
318
+ # Conceitos:
319
+ #
320
+ # PID FILE
321
+ # Armazena o PID do processo ativo.
322
+ # Usado para garantir instância única e controle externo.
323
+ #
324
+ # STOP FILE
325
+ # Arquivo sentinela para sinalizar encerramento gracioso.
326
+ # O processo monitora sua existência periodicamente.
327
+ #
328
+ # HEARTBEAT FILE
329
+ # Atualizado continuamente pelo processo.
330
+ # Permite verificar se o serviço está vivo ou travado.
331
+ #
332
+ # Diretório base:
333
+ # %APPDATA%/mtcli/run (Windows)
334
+ # ~/.mtcli/run (fallback)
335
+ #
336
+ # Cada serviço define seus próprios arquivos (ex: ticks, risco).
337
+ #
338
+ # Exemplo:
339
+ # ticks.pid
340
+ # ticks.stop
341
+ # ticks.heartbeat
342
+ #
314
343
 
315
344
  RUN_DIR = os.path.join(
316
345
  os.getenv("APPDATA", os.path.expanduser("~")),
@@ -0,0 +1,136 @@
1
+ """
2
+ Sistema central de logging do mtcli.
3
+
4
+ Versão corrigida para evitar duplicação de logs em cenários com:
5
+
6
+ - múltiplos plugins
7
+ - múltiplos loggers
8
+ - integração com pytest (caplog)
9
+ - uso de logging básico por libs externas
10
+
11
+ Estratégia adotada
12
+ ------------------
13
+
14
+ - Um único handler é configurado no ROOT logger
15
+ - Todos os loggers filhos propagam para o root
16
+ - Nenhum handler é anexado diretamente aos loggers de módulo
17
+
18
+ Isso elimina completamente duplicação de logs.
19
+
20
+ API permanece 100% compatível.
21
+ """
22
+
23
+ import logging
24
+ from logging.handlers import RotatingFileHandler
25
+ import os
26
+ from pathlib import Path
27
+
28
+
29
+ # ==========================================================
30
+ # DIRETÓRIO DE LOG
31
+ # ==========================================================
32
+
33
+ base_dir = os.getenv("APPDATA", os.path.expanduser("~"))
34
+
35
+ LOG_DIR = Path(base_dir) / "mtcli" / "logs"
36
+ LOG_DIR.mkdir(parents=True, exist_ok=True)
37
+
38
+
39
+ # ==========================================================
40
+ # RESOLUÇÃO DO ARQUIVO
41
+ # ==========================================================
42
+
43
+ def _resolve_log_file() -> Path:
44
+ log_name = os.getenv("MTCLI_LOG_NAME", "mtcli")
45
+ per_process = os.getenv("MTCLI_LOG_PER_PROCESS")
46
+
47
+ if per_process:
48
+ return LOG_DIR / f"{log_name}-{os.getpid()}.log"
49
+
50
+ return LOG_DIR / f"{log_name}.log"
51
+
52
+
53
+ LOG_FILE = _resolve_log_file()
54
+
55
+
56
+ # ==========================================================
57
+ # CONTROLE GLOBAL (ANTI DUPLICAÇÃO)
58
+ # ==========================================================
59
+
60
+ _MTCLI_LOGGER_CONFIGURED = False
61
+
62
+
63
+ # ==========================================================
64
+ # SETUP
65
+ # ==========================================================
66
+
67
+ def setup_logger(name: str = "mtcli") -> logging.Logger:
68
+ """
69
+ Retorna logger configurado.
70
+
71
+ A configuração real ocorre apenas uma vez no ROOT logger.
72
+
73
+ Parameters
74
+ ----------
75
+ name : str
76
+ Nome do logger.
77
+
78
+ Returns
79
+ -------
80
+ logging.Logger
81
+ """
82
+
83
+ global _MTCLI_LOGGER_CONFIGURED
84
+
85
+ root = logging.getLogger()
86
+
87
+ # ------------------------------------------------------
88
+ # CONFIGURAÇÃO GLOBAL (executa uma única vez)
89
+ # ------------------------------------------------------
90
+
91
+ if not _MTCLI_LOGGER_CONFIGURED:
92
+
93
+ root.setLevel(logging.DEBUG)
94
+
95
+ formatter = logging.Formatter(
96
+ "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
97
+ datefmt="%Y-%m-%d %H:%M:%S",
98
+ )
99
+
100
+ # remove handlers existentes (evita duplicação externa)
101
+ for h in list(root.handlers):
102
+ root.removeHandler(h)
103
+
104
+ file_handler = RotatingFileHandler(
105
+ LOG_FILE,
106
+ maxBytes=2_000_000,
107
+ backupCount=3,
108
+ encoding="utf-8",
109
+ delay=True,
110
+ )
111
+
112
+ file_handler.setFormatter(formatter)
113
+
114
+ root.addHandler(file_handler)
115
+
116
+ _MTCLI_LOGGER_CONFIGURED = True
117
+
118
+ # ------------------------------------------------------
119
+ # LOGGER FILHO (sem handler próprio)
120
+ # ------------------------------------------------------
121
+
122
+ logger = logging.getLogger(name)
123
+
124
+ logger.setLevel(logging.DEBUG)
125
+
126
+ # 🔥 CRÍTICO: não anexar handler aqui
127
+ logger.propagate = True
128
+
129
+ return logger
130
+
131
+
132
+ # ==========================================================
133
+ # LOGGER PADRÃO
134
+ # ==========================================================
135
+
136
+ log = setup_logger()
@@ -0,0 +1,65 @@
1
+ """
2
+ Utilitários de conversão de tempo para o mtcli.
3
+
4
+ Este módulo fornece funções para:
5
+ - Obter o horário atual em UTC
6
+ - Converter datas de UTC para o timezone da B3 (Brasil)
7
+ - Converter datas da B3 para UTC
8
+
9
+ Todas as funções trabalham com objetos datetime timezone-aware.
10
+ """
11
+
12
+ from datetime import datetime
13
+ from zoneinfo import ZoneInfo
14
+
15
+
16
+ # Timezones padrão
17
+ UTC = ZoneInfo("UTC")
18
+ B3_TZ = ZoneInfo("America/Sao_Paulo")
19
+
20
+
21
+ def now_utc():
22
+ """
23
+ Retorna o horário atual em UTC.
24
+
25
+ Returns:
26
+ datetime: Data/hora atual com timezone UTC (aware).
27
+ """
28
+ return datetime.now(tz=UTC)
29
+
30
+
31
+ def utc_to_b3(dt):
32
+ """
33
+ Converte um datetime em UTC para o timezone da B3.
34
+
35
+ Caso o datetime seja naive (sem timezone), assume-se que ele já está em UTC.
36
+
37
+ Args:
38
+ dt (datetime): Data/hora em UTC (aware ou naive).
39
+
40
+ Returns:
41
+ datetime: Data/hora convertida para o timezone da B3.
42
+ """
43
+ if dt.tzinfo is None:
44
+ dt = dt.replace(tzinfo=UTC)
45
+
46
+ return dt.astimezone(B3_TZ)
47
+
48
+
49
+ def b3_to_utc(dt):
50
+ """
51
+ Converte um datetime do timezone da B3 para UTC.
52
+
53
+ Caso o datetime seja naive (sem timezone), assume-se que ele já está
54
+ no horário da B3 (America/Sao_Paulo).
55
+
56
+ Args:
57
+ dt (datetime): Data/hora no timezone da B3 (aware ou naive).
58
+
59
+ Returns:
60
+ datetime: Data/hora convertida para UTC.
61
+ """
62
+ if dt.tzinfo is None:
63
+ dt = dt.replace(tzinfo=B3_TZ)
64
+
65
+ return dt.astimezone(UTC)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mtcli"
3
- version = "3.7.2"
3
+ version = "3.7.3"
4
4
  description = "Aplicativo CLI para exibir gráficos do MetaTrader 5 screen reader friendly"
5
5
  authors = [
6
6
  {name = "Valmir França",email = "vfranca3@gmail.com"}
@@ -34,6 +34,7 @@ classifiers = [
34
34
  dependencies = [
35
35
  "click (>=8.2.1,<9.0.0)",
36
36
  "metatrader5 (>=5.0.5260,<6.0.0)",
37
+ "tzdata (>=2025.3,<2026.0)",
37
38
  ]
38
39
 
39
40
  [project.urls]
@@ -45,6 +46,7 @@ issues = "https://github.com/vfranca/mtcli/issues"
45
46
  [project.scripts]
46
47
  mtcli = "mtcli.cli:mt"
47
48
  mt = "mtcli.cli:mt"
49
+ mtdev = "mtcli.cli_dev:cli"
48
50
 
49
51
  [project.entry-points."mtcli.plugins"]
50
52
  internals = "mtcli.plugin:register"
mtcli-3.7.2/mtcli/cli.py DELETED
@@ -1,74 +0,0 @@
1
- """
2
- CLI principal do mtcli.
3
-
4
- Este módulo define o grupo principal `mt`
5
- e inicializa o carregamento de plugins.
6
- """
7
-
8
- import click
9
-
10
- from mtcli.plugin_loader import load_plugins
11
- from mtcli.logger import setup_logger
12
-
13
- from .commands.bars import bars
14
- from .commands.doctor import doctor
15
-
16
- logger = setup_logger(__name__)
17
-
18
-
19
- @click.group(context_settings={"max_content_width": 120})
20
- @click.version_option(package_name="mtcli")
21
- def mt():
22
- """
23
- CLI principal do mtcli.
24
-
25
- Exibe gráficos e informações de mercado
26
- em formato textual compatível com leitores de tela.
27
- """
28
- pass
29
-
30
-
31
- # ---------------------------------------------------------
32
- # Comandos internos
33
- # ---------------------------------------------------------
34
-
35
- mt.add_command(doctor, name="doctor")
36
- mt.add_command(bars, name="bars")
37
- mt.add_command(doctor, name="dr")
38
-
39
-
40
- # ---------------------------------------------------------
41
- # Carregamento de plugins
42
- # ---------------------------------------------------------
43
-
44
- loaded_plugins = load_plugins(mt)
45
-
46
- logger.info("Plugins carregados: %s", loaded_plugins)
47
-
48
-
49
- # ---------------------------------------------------------
50
- # Comando utilitário: listar plugins
51
- # ---------------------------------------------------------
52
-
53
- @mt.command(name="plugins")
54
- def list_plugins():
55
- """
56
- Lista os plugins atualmente carregados no mtcli.
57
- """
58
-
59
- if not loaded_plugins:
60
- click.echo("Nenhum plugin carregado.")
61
- return
62
-
63
- click.echo("Plugins carregados:\n")
64
-
65
- for name in loaded_plugins:
66
- click.echo(f" {name}")
67
-
68
-
69
- # ---------------------------------------------------------
70
- # Entry point
71
- # ---------------------------------------------------------
72
-
73
- if __name__ == "__main__":
74
- mt()
@@ -1,54 +0,0 @@
1
- """
2
- Database core para mtcli.
3
-
4
- Responsável por:
5
- - Criar conexão SQLite
6
- - Ativar WAL
7
- - Garantir schema
8
- """
9
-
10
- import sqlite3
11
- from pathlib import Path
12
-
13
-
14
- DB_PATH = Path.home() / ".mtcli" / "marketdata.db"
15
-
16
-
17
- def get_connection():
18
- """
19
- Retorna conexão SQLite configurada.
20
- """
21
- DB_PATH.parent.mkdir(parents=True, exist_ok=True)
22
-
23
- conn = sqlite3.connect(DB_PATH)
24
- conn.execute("PRAGMA journal_mode=WAL;")
25
- conn.execute("PRAGMA synchronous=NORMAL;")
26
-
27
- _ensure_schema(conn)
28
- return conn
29
-
30
-
31
- def _ensure_schema(conn):
32
- conn.execute(
33
- """
34
- CREATE TABLE IF NOT EXISTS ticks (
35
- symbol TEXT NOT NULL,
36
- time INTEGER NOT NULL,
37
- bid REAL,
38
- ask REAL,
39
- last REAL,
40
- volume REAL,
41
- flags INTEGER,
42
- PRIMARY KEY (symbol, time)
43
- );
44
- """
45
- )
46
-
47
- conn.execute(
48
- """
49
- CREATE INDEX IF NOT EXISTS idx_ticks_symbol_time
50
- ON ticks(symbol, time);
51
- """
52
- )
53
-
54
- conn.commit()
@@ -1,178 +0,0 @@
1
- """
2
- Sistema central de logging do mtcli.
3
-
4
- Este módulo fornece uma função única `setup_logger()` utilizada por todo
5
- o ecossistema de plugins do mtcli para configurar logging consistente.
6
-
7
- Características principais
8
- --------------------------
9
-
10
- * Arquivo de log rotativo em:
11
- %APPDATA%/mtcli/logs/
12
-
13
- * Rotação automática:
14
- - tamanho máximo: 2 MB
15
- - até 3 arquivos de backup
16
-
17
- * Proteção contra duplicação de handlers quando plugins
18
- inicializam o logger múltiplas vezes.
19
-
20
- * Encoding UTF-8 garantido (evita problemas de acentuação
21
- no Windows).
22
-
23
- * Compatível com pytest (caplog).
24
-
25
- * Suporte opcional a logs por processo (multi-process safe)
26
-
27
- Variáveis de ambiente
28
- ---------------------
29
-
30
- MTCLI_LOG_PER_PROCESS=1
31
- cria arquivos separados por PID
32
- exemplo: mtcli-1234.log
33
-
34
- MTCLI_LOG_NAME=risco
35
- define nome base do arquivo de log
36
-
37
- Observação
38
- ----------
39
-
40
- Os logs **não são exibidos no console**.
41
- Toda saída é direcionada exclusivamente para arquivo.
42
- """
43
-
44
- import logging
45
- from logging.handlers import RotatingFileHandler
46
- import os
47
- from pathlib import Path
48
-
49
-
50
- # ==========================================================
51
- # DIRETÓRIO DE LOG
52
- # ==========================================================
53
-
54
- base_dir = os.getenv("APPDATA", os.path.expanduser("~"))
55
-
56
- LOG_DIR = Path(base_dir) / "mtcli" / "logs"
57
-
58
- LOG_DIR.mkdir(parents=True, exist_ok=True)
59
-
60
-
61
- # ==========================================================
62
- # RESOLUÇÃO DO NOME DO LOG
63
- # ==========================================================
64
-
65
- def _resolve_log_file() -> Path:
66
- """
67
- Resolve dinamicamente o caminho do arquivo de log.
68
-
69
- Mantém compatibilidade com versões anteriores
70
- mas permite novos modos via variáveis de ambiente.
71
- """
72
-
73
- log_name = os.getenv("MTCLI_LOG_NAME", "mtcli")
74
-
75
- per_process = os.getenv("MTCLI_LOG_PER_PROCESS")
76
-
77
- if per_process:
78
- pid = os.getpid()
79
- filename = f"{log_name}-{pid}.log"
80
- else:
81
- filename = f"{log_name}.log"
82
-
83
- return LOG_DIR / filename
84
-
85
-
86
- LOG_FILE = _resolve_log_file()
87
-
88
-
89
- # ==========================================================
90
- # LOGGER SETUP
91
- # ==========================================================
92
-
93
- def setup_logger(name: str = "mtcli") -> logging.Logger:
94
- """
95
- Cria ou retorna um logger configurado para o mtcli.
96
-
97
- O logger utiliza **apenas um handler de arquivo rotativo**.
98
- Nenhuma saída é enviada ao console.
99
-
100
- A função é **idempotente**, ou seja, pode ser chamada
101
- múltiplas vezes sem duplicar handlers.
102
-
103
- Parameters
104
- ----------
105
- name : str
106
- Nome do logger (normalmente `__name__`).
107
-
108
- Returns
109
- -------
110
- logging.Logger
111
- Instância configurada do logger.
112
- """
113
-
114
- logger = logging.getLogger(name)
115
-
116
- logger.setLevel(logging.DEBUG)
117
-
118
- formatter = logging.Formatter(
119
- "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
120
- datefmt="%Y-%m-%d %H:%M:%S",
121
- )
122
-
123
- # ======================================================
124
- # REMOVE STREAM HANDLERS (silencia console)
125
- # ======================================================
126
-
127
- for handler in list(logger.handlers):
128
-
129
- if isinstance(handler, logging.StreamHandler) and not isinstance(
130
- handler, RotatingFileHandler
131
- ):
132
- logger.removeHandler(handler)
133
-
134
- # ======================================================
135
- # EVITA DUPLICAÇÃO DE FILE HANDLER
136
- # ======================================================
137
-
138
- for handler in logger.handlers:
139
-
140
- if isinstance(handler, RotatingFileHandler):
141
-
142
- try:
143
- if Path(handler.baseFilename) == LOG_FILE:
144
- return logger
145
- except Exception:
146
- pass
147
-
148
- # ======================================================
149
- # FILE HANDLER ROTATIVO
150
- # ======================================================
151
-
152
- file_handler = RotatingFileHandler(
153
- LOG_FILE,
154
- maxBytes=2_000_000,
155
- backupCount=3,
156
- encoding="utf-8",
157
- delay=True,
158
- )
159
-
160
- file_handler.setFormatter(formatter)
161
-
162
- logger.addHandler(file_handler)
163
-
164
- # ======================================================
165
- # PROPAGATION
166
- # ======================================================
167
-
168
- # Permite que pytest caplog capture logs
169
- logger.propagate = True
170
-
171
- return logger
172
-
173
-
174
- # ==========================================================
175
- # LOGGER PADRÃO DO MTCLI
176
- # ==========================================================
177
-
178
- log = setup_logger()
@@ -1,24 +0,0 @@
1
- """
2
- Cache de ticks em memória.
3
- """
4
-
5
- from collections import deque
6
-
7
-
8
- class TickCache:
9
- """
10
- Mantém janela recente de ticks em memória.
11
- """
12
-
13
- def __init__(self, max_size=10000):
14
- self.buffer = deque(maxlen=max_size)
15
-
16
- def add_many(self, ticks):
17
- for t in ticks:
18
- self.buffer.append(t)
19
-
20
- def get_all(self):
21
- return list(self.buffer)
22
-
23
- def clear(self):
24
- self.buffer.clear()
@@ -1,142 +0,0 @@
1
- """
2
- TickRepository profissional com:
3
-
4
- - Paginação automática
5
- - Sincronização incremental robusta
6
- - Integração com mt5_conexao
7
- - Proteção contra loops infinitos
8
- """
9
-
10
- import MetaTrader5 as mt5
11
- from datetime import datetime, timedelta
12
-
13
- from ..database import get_connection
14
- from .tick_cache import TickCache
15
- from mtcli.mt5_context import mt5_conexao
16
-
17
-
18
- class TickRepository:
19
-
20
- BATCH_SIZE = 200000 # tamanho seguro por lote
21
-
22
- def __init__(self):
23
- self.conn = get_connection()
24
- self.cache = TickCache()
25
-
26
- # ============================================================
27
- # SINCRONIZAÇÃO COM PAGINAÇÃO
28
- # ============================================================
29
-
30
- def sync(self, symbol: str, days_back: int = 1):
31
- """
32
- Sincroniza banco com MT5 usando paginação.
33
-
34
- Busca todos os ticks disponíveis desde o último timestamp.
35
- """
36
-
37
- total_inserted = 0
38
-
39
- with mt5_conexao():
40
-
41
- last_time = self._get_last_tick_time(symbol)
42
-
43
- if last_time:
44
- start = datetime.fromtimestamp(last_time + 1)
45
- else:
46
- start = datetime.now() - timedelta(days=days_back)
47
-
48
- while True:
49
-
50
- ticks = mt5.copy_ticks_from(
51
- symbol,
52
- start,
53
- self.BATCH_SIZE,
54
- mt5.COPY_TICKS_ALL,
55
- )
56
-
57
- if ticks is None or len(ticks) == 0:
58
- break
59
-
60
- inserted = self._insert_ticks(symbol, ticks)
61
- total_inserted += inserted
62
-
63
- self.cache.add_many(ticks)
64
-
65
- # Atualiza início para próximo tick
66
- ultimo_timestamp = int(ticks[-1]["time"])
67
- novo_start = datetime.fromtimestamp(ultimo_timestamp + 1)
68
-
69
- # Proteção contra loop infinito
70
- if novo_start <= start:
71
- break
72
-
73
- start = novo_start
74
-
75
- # Se retornou menos que o batch, acabou histórico
76
- if len(ticks) < self.BATCH_SIZE:
77
- break
78
-
79
- return total_inserted
80
-
81
- # ============================================================
82
- # INSERÇÃO
83
- # ============================================================
84
-
85
- def _insert_ticks(self, symbol, ticks):
86
-
87
- cursor = self.conn.cursor()
88
-
89
- data = [
90
- (
91
- symbol,
92
- int(t["time"]),
93
- float(t["bid"]),
94
- float(t["ask"]),
95
- float(t["last"]),
96
- float(t["volume"]),
97
- int(t["flags"]),
98
- )
99
- for t in ticks
100
- ]
101
-
102
- cursor.executemany(
103
- """
104
- INSERT OR IGNORE INTO ticks
105
- VALUES (?, ?, ?, ?, ?, ?, ?)
106
- """,
107
- data,
108
- )
109
-
110
- self.conn.commit()
111
- return cursor.rowcount
112
-
113
- # ============================================================
114
- # CONSULTAS
115
- # ============================================================
116
-
117
- def get_ticks_between(self, symbol, start_ts, end_ts):
118
- cursor = self.conn.cursor()
119
- cursor.execute(
120
- """
121
- SELECT time, bid, ask, last, volume, flags
122
- FROM ticks
123
- WHERE symbol = ?
124
- AND time BETWEEN ? AND ?
125
- ORDER BY time ASC
126
- """,
127
- (symbol, start_ts, end_ts),
128
- )
129
- return cursor.fetchall()
130
-
131
- def _get_last_tick_time(self, symbol):
132
- cursor = self.conn.cursor()
133
- cursor.execute(
134
- """
135
- SELECT MAX(time)
136
- FROM ticks
137
- WHERE symbol = ?
138
- """,
139
- (symbol,),
140
- )
141
- result = cursor.fetchone()
142
- return result[0] if result and result[0] else None
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