mtcli 3.7.1__tar.gz → 3.8.0.dev0__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.7.1 → mtcli-3.8.0.dev0}/PKG-INFO +1 -1
- mtcli-3.8.0.dev0/mtcli/cli.py +135 -0
- mtcli-3.8.0.dev0/mtcli/commands/migrate.py +12 -0
- mtcli-3.8.0.dev0/mtcli/database.py +66 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/logger.py +136 -136
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/marketdata/tick_cache.py +38 -24
- mtcli-3.8.0.dev0/mtcli/marketdata/tick_repository.py +175 -0
- mtcli-3.8.0.dev0/mtcli/marketdata/tick_streamer.py +75 -0
- mtcli-3.8.0.dev0/mtcli/migrations/001_initial_schema.py +21 -0
- mtcli-3.8.0.dev0/mtcli/migrations/002_ticks_time_msc.py +21 -0
- mtcli-3.8.0.dev0/mtcli/migrations/003_optimize_ticks_without_rowid.py +63 -0
- mtcli-3.8.0.dev0/mtcli/migrations/runner.py +104 -0
- mtcli-3.8.0.dev0/mtcli/plugins/volume_medio/tests/__init__.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/pyproject.toml +1 -1
- mtcli-3.7.1/mtcli/cli.py +0 -74
- mtcli-3.7.1/mtcli/database.py +0 -54
- mtcli-3.7.1/mtcli/marketdata/tick_repository.py +0 -142
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/LICENSE +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/README.md +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/__init__.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/commands/__init__.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/commands/bars.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/commands/conf.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/commands/doctor.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/conecta.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/conf.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/config_registre.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/data/__init__.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/data/base.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/data/csv.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/data/mt5.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/domain/__init__.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/domain/timeframe.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/marketdata/__init__.py +0 -0
- {mtcli-3.7.1/mtcli/plugins → mtcli-3.8.0.dev0/mtcli/migrations}/__init__.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/models/__init__.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/models/bar_model.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/models/bars_model.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/models/chart_model.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/models/conf_model.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/models/consecutive_bars_model.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/models/rates_model.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/models/signals_model.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/models/unconsecutive_bar_model.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/mt5_context.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugin.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugin_loader.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugin_manager.py +0 -0
- {mtcli-3.7.1/mtcli/plugins/media_movel/tests → mtcli-3.8.0.dev0/mtcli/plugins}/__init__.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/exemplo.py-dist +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/media_movel/__init__.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/media_movel/cli.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/media_movel/conf.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/media_movel/models/__init__.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/media_movel/models/model_media_movel.py +0 -0
- {mtcli-3.7.1/mtcli/plugins/range_medio/models → mtcli-3.8.0.dev0/mtcli/plugins/media_movel/tests}/__init__.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/media_movel/tests/test_mm.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/media_movel/tests/test_model_media_movel.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/range_medio/__init__.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/range_medio/cli.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/range_medio/conf.py +0 -0
- {mtcli-3.7.1/mtcli/plugins/range_medio/tests → mtcli-3.8.0.dev0/mtcli/plugins/range_medio/models}/__init__.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/range_medio/models/average_range_model.py +0 -0
- {mtcli-3.7.1/mtcli/plugins/volume_medio → mtcli-3.8.0.dev0/mtcli/plugins/range_medio}/tests/__init__.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/range_medio/tests/test_rm.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/volume_medio/__init__.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/volume_medio/cli.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/volume_medio/conf.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/volume_medio/models/__init__.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/volume_medio/models/model_average_volume.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/plugins/volume_medio/tests/test_vm.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/views/__init__.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/views/close_view.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/views/full_view.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/views/high_view.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/views/low_view.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/views/min_view.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/views/open_view.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/views/ranges_view.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/views/rates_view.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/views/vars_view.py +0 -0
- {mtcli-3.7.1 → mtcli-3.8.0.dev0}/mtcli/views/volumes_view.py +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
Também inicia a captura contínua de ticks para manter
|
|
8
|
+
um histórico próprio independente do broker.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import threading
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
from mtcli.plugin_loader import load_plugins
|
|
16
|
+
from mtcli.logger import setup_logger
|
|
17
|
+
from mtcli.marketdata.tick_streamer import TickStreamer
|
|
18
|
+
|
|
19
|
+
from .commands.bars import bars
|
|
20
|
+
from .commands.doctor import doctor
|
|
21
|
+
from .commands.migrate import migrate
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
logger = setup_logger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------
|
|
28
|
+
# Configuração de captura de ticks
|
|
29
|
+
# ---------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
_tick_streamer = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def start_tick_capture():
|
|
35
|
+
"""
|
|
36
|
+
Inicia captura contínua de ticks em background.
|
|
37
|
+
|
|
38
|
+
O símbolo pode ser configurado via variável de ambiente:
|
|
39
|
+
|
|
40
|
+
MTCLI_SYMBOL=WINJ26
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
global _tick_streamer
|
|
44
|
+
|
|
45
|
+
if _tick_streamer:
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
symbol = os.getenv("SYMBOL")
|
|
49
|
+
|
|
50
|
+
if not symbol:
|
|
51
|
+
logger.info("Captura de ticks desativada (MTCLI_SYMBOL não definido).")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
logger.info("Iniciando captura contínua de ticks para %s", symbol)
|
|
55
|
+
|
|
56
|
+
_tick_streamer = TickStreamer(symbol)
|
|
57
|
+
|
|
58
|
+
thread = threading.Thread(
|
|
59
|
+
target=_tick_streamer.start,
|
|
60
|
+
daemon=True,
|
|
61
|
+
name="mtcli-tick-streamer",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
thread.start()
|
|
65
|
+
|
|
66
|
+
logger.info("Captura de ticks iniciada em background.")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------
|
|
70
|
+
# CLI principal
|
|
71
|
+
# ---------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
@click.group(context_settings={"max_content_width": 120}, invoke_without_command=True)
|
|
74
|
+
@click.version_option(package_name="mtcli")
|
|
75
|
+
@click.pass_context
|
|
76
|
+
def mt(ctx):
|
|
77
|
+
"""
|
|
78
|
+
CLI principal do mtcli.
|
|
79
|
+
|
|
80
|
+
Exibe gráficos e informações de mercado
|
|
81
|
+
em formato textual compatível com leitores de tela.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
# inicia captura de ticks
|
|
85
|
+
start_tick_capture()
|
|
86
|
+
|
|
87
|
+
if ctx.invoked_subcommand is None:
|
|
88
|
+
click.echo(ctx.get_help())
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------
|
|
92
|
+
# Comandos internos
|
|
93
|
+
# ---------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
mt.add_command(doctor, name="doctor")
|
|
96
|
+
mt.add_command(bars, name="bars")
|
|
97
|
+
mt.add_command(doctor, name="dr")
|
|
98
|
+
mt.add_command(migrate)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------
|
|
102
|
+
# Carregamento de plugins
|
|
103
|
+
# ---------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
loaded_plugins = load_plugins(mt)
|
|
106
|
+
|
|
107
|
+
logger.info("Plugins carregados: %s", loaded_plugins)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ---------------------------------------------------------
|
|
111
|
+
# Comando utilitário: listar plugins
|
|
112
|
+
# ---------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
@mt.command(name="plugins")
|
|
115
|
+
def list_plugins():
|
|
116
|
+
"""
|
|
117
|
+
Lista os plugins atualmente carregados no mtcli.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
if not loaded_plugins:
|
|
121
|
+
click.echo("Nenhum plugin carregado.")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
click.echo("Plugins carregados:\n")
|
|
125
|
+
|
|
126
|
+
for name in loaded_plugins:
|
|
127
|
+
click.echo(f" {name}")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------
|
|
131
|
+
# Entry point
|
|
132
|
+
# ---------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
mt()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
DB_PATH = Path.home() / ".mtcli" / "marketdata.db"
|
|
6
|
+
BACKUP_DIR = Path.home() / ".mtcli" / "backups"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_connection():
|
|
10
|
+
|
|
11
|
+
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
|
|
13
|
+
|
|
14
|
+
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
|
15
|
+
|
|
16
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
17
|
+
conn.execute("PRAGMA synchronous=NORMAL")
|
|
18
|
+
conn.execute("PRAGMA temp_store=MEMORY")
|
|
19
|
+
conn.execute("PRAGMA mmap_size=30000000000")
|
|
20
|
+
conn.execute("PRAGMA cache_size=-200000")
|
|
21
|
+
|
|
22
|
+
conn.execute("""
|
|
23
|
+
CREATE TABLE IF NOT EXISTS schema_migrations(
|
|
24
|
+
version INTEGER PRIMARY KEY,
|
|
25
|
+
applied_at TEXT NOT NULL
|
|
26
|
+
)
|
|
27
|
+
""")
|
|
28
|
+
|
|
29
|
+
conn.commit()
|
|
30
|
+
|
|
31
|
+
return conn
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ==========================================================
|
|
35
|
+
# CHECKPOINT
|
|
36
|
+
# ==========================================================
|
|
37
|
+
|
|
38
|
+
def wal_checkpoint(conn):
|
|
39
|
+
"""
|
|
40
|
+
Reduz tamanho do WAL e mantém banco saudável.
|
|
41
|
+
"""
|
|
42
|
+
conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ==========================================================
|
|
46
|
+
# BACKUP
|
|
47
|
+
# ==========================================================
|
|
48
|
+
|
|
49
|
+
def backup_database(conn):
|
|
50
|
+
"""
|
|
51
|
+
Backup diário seguro do banco SQLite.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
now = datetime.now().strftime("%Y%m%d")
|
|
55
|
+
|
|
56
|
+
backup_path = BACKUP_DIR / f"marketdata_{now}.db"
|
|
57
|
+
|
|
58
|
+
if backup_path.exists():
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
backup_conn = sqlite3.connect(backup_path)
|
|
62
|
+
|
|
63
|
+
with backup_conn:
|
|
64
|
+
conn.backup(backup_conn)
|
|
65
|
+
|
|
66
|
+
backup_conn.close()
|
|
@@ -1,136 +1,136 @@
|
|
|
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/mtcli.log
|
|
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
|
-
Observação
|
|
26
|
-
----------
|
|
27
|
-
|
|
28
|
-
Os logs **não são exibidos no console**.
|
|
29
|
-
Toda saída é direcionada exclusivamente para o arquivo de log.
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
import logging
|
|
33
|
-
from logging.handlers import RotatingFileHandler
|
|
34
|
-
import os
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# ==========================================================
|
|
38
|
-
# DIRETÓRIO DE LOG
|
|
39
|
-
# ==========================================================
|
|
40
|
-
|
|
41
|
-
base_dir = os.getenv("APPDATA", os.path.expanduser("~"))
|
|
42
|
-
|
|
43
|
-
LOG_DIR = os.path.join(base_dir, "mtcli", "logs")
|
|
44
|
-
|
|
45
|
-
os.makedirs(LOG_DIR, exist_ok=True)
|
|
46
|
-
|
|
47
|
-
LOG_FILE = os.path.join(LOG_DIR, "mtcli.log")
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
# ==========================================================
|
|
51
|
-
# LOGGER SETUP
|
|
52
|
-
# ==========================================================
|
|
53
|
-
|
|
54
|
-
def setup_logger(name: str = "mtcli") -> logging.Logger:
|
|
55
|
-
"""
|
|
56
|
-
Cria ou retorna um logger configurado para o mtcli.
|
|
57
|
-
|
|
58
|
-
O logger utiliza **apenas um handler de arquivo rotativo**.
|
|
59
|
-
Nenhuma saída é enviada ao console.
|
|
60
|
-
|
|
61
|
-
A função é **idempotente**, ou seja, pode ser chamada
|
|
62
|
-
múltiplas vezes sem duplicar handlers.
|
|
63
|
-
|
|
64
|
-
Parameters
|
|
65
|
-
----------
|
|
66
|
-
name : str
|
|
67
|
-
Nome do logger (normalmente `__name__`).
|
|
68
|
-
|
|
69
|
-
Returns
|
|
70
|
-
-------
|
|
71
|
-
logging.Logger
|
|
72
|
-
Instância configurada do logger.
|
|
73
|
-
"""
|
|
74
|
-
|
|
75
|
-
logger = logging.getLogger(name)
|
|
76
|
-
|
|
77
|
-
logger.setLevel(logging.DEBUG)
|
|
78
|
-
|
|
79
|
-
formatter = logging.Formatter(
|
|
80
|
-
"%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
|
|
81
|
-
datefmt="%Y-%m-%d %H:%M:%S",
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
# ======================================================
|
|
85
|
-
# REMOVE STREAM HANDLERS (garante silêncio no console)
|
|
86
|
-
# ======================================================
|
|
87
|
-
|
|
88
|
-
for handler in list(logger.handlers):
|
|
89
|
-
if isinstance(handler, logging.StreamHandler) and not isinstance(handler, RotatingFileHandler):
|
|
90
|
-
logger.removeHandler(handler)
|
|
91
|
-
|
|
92
|
-
# ======================================================
|
|
93
|
-
# FILE HANDLER ROTATIVO
|
|
94
|
-
# ======================================================
|
|
95
|
-
|
|
96
|
-
file_handler_exists = any(
|
|
97
|
-
isinstance(h, RotatingFileHandler) for h in logger.handlers
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
if not file_handler_exists:
|
|
101
|
-
|
|
102
|
-
file_handler = RotatingFileHandler(
|
|
103
|
-
LOG_FILE,
|
|
104
|
-
maxBytes=2_000_000,
|
|
105
|
-
backupCount=3,
|
|
106
|
-
encoding="utf-8",
|
|
107
|
-
delay=True,
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
file_handler.setFormatter(formatter)
|
|
111
|
-
|
|
112
|
-
logger.addHandler(file_handler)
|
|
113
|
-
|
|
114
|
-
# ======================================================
|
|
115
|
-
# PROPAGATION
|
|
116
|
-
# ======================================================
|
|
117
|
-
|
|
118
|
-
# Permite que pytest caplog capture logs
|
|
119
|
-
logger.propagate = True
|
|
120
|
-
|
|
121
|
-
return logger
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
# ==========================================================
|
|
125
|
-
# LOGGER PADRÃO DO MTCLI
|
|
126
|
-
# ==========================================================
|
|
127
|
-
|
|
128
|
-
"""
|
|
129
|
-
Logger padrão utilizado por módulos internos do mtcli.
|
|
130
|
-
|
|
131
|
-
Plugins geralmente criam seu próprio logger usando:
|
|
132
|
-
|
|
133
|
-
setup_logger(__name__)
|
|
134
|
-
"""
|
|
135
|
-
|
|
136
|
-
log = setup_logger()
|
|
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/mtcli.log
|
|
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
|
+
Observação
|
|
26
|
+
----------
|
|
27
|
+
|
|
28
|
+
Os logs **não são exibidos no console**.
|
|
29
|
+
Toda saída é direcionada exclusivamente para o arquivo de log.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import logging
|
|
33
|
+
from logging.handlers import RotatingFileHandler
|
|
34
|
+
import os
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ==========================================================
|
|
38
|
+
# DIRETÓRIO DE LOG
|
|
39
|
+
# ==========================================================
|
|
40
|
+
|
|
41
|
+
base_dir = os.getenv("APPDATA", os.path.expanduser("~"))
|
|
42
|
+
|
|
43
|
+
LOG_DIR = os.path.join(base_dir, "mtcli", "logs")
|
|
44
|
+
|
|
45
|
+
os.makedirs(LOG_DIR, exist_ok=True)
|
|
46
|
+
|
|
47
|
+
LOG_FILE = os.path.join(LOG_DIR, "mtcli.log")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ==========================================================
|
|
51
|
+
# LOGGER SETUP
|
|
52
|
+
# ==========================================================
|
|
53
|
+
|
|
54
|
+
def setup_logger(name: str = "mtcli") -> logging.Logger:
|
|
55
|
+
"""
|
|
56
|
+
Cria ou retorna um logger configurado para o mtcli.
|
|
57
|
+
|
|
58
|
+
O logger utiliza **apenas um handler de arquivo rotativo**.
|
|
59
|
+
Nenhuma saída é enviada ao console.
|
|
60
|
+
|
|
61
|
+
A função é **idempotente**, ou seja, pode ser chamada
|
|
62
|
+
múltiplas vezes sem duplicar handlers.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
name : str
|
|
67
|
+
Nome do logger (normalmente `__name__`).
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
logging.Logger
|
|
72
|
+
Instância configurada do logger.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
logger = logging.getLogger(name)
|
|
76
|
+
|
|
77
|
+
logger.setLevel(logging.DEBUG)
|
|
78
|
+
|
|
79
|
+
formatter = logging.Formatter(
|
|
80
|
+
"%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
|
|
81
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# ======================================================
|
|
85
|
+
# REMOVE STREAM HANDLERS (garante silêncio no console)
|
|
86
|
+
# ======================================================
|
|
87
|
+
|
|
88
|
+
for handler in list(logger.handlers):
|
|
89
|
+
if isinstance(handler, logging.StreamHandler) and not isinstance(handler, RotatingFileHandler):
|
|
90
|
+
logger.removeHandler(handler)
|
|
91
|
+
|
|
92
|
+
# ======================================================
|
|
93
|
+
# FILE HANDLER ROTATIVO
|
|
94
|
+
# ======================================================
|
|
95
|
+
|
|
96
|
+
file_handler_exists = any(
|
|
97
|
+
isinstance(h, RotatingFileHandler) for h in logger.handlers
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if not file_handler_exists:
|
|
101
|
+
|
|
102
|
+
file_handler = RotatingFileHandler(
|
|
103
|
+
LOG_FILE,
|
|
104
|
+
maxBytes=2_000_000,
|
|
105
|
+
backupCount=3,
|
|
106
|
+
encoding="utf-8",
|
|
107
|
+
delay=True,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
file_handler.setFormatter(formatter)
|
|
111
|
+
|
|
112
|
+
logger.addHandler(file_handler)
|
|
113
|
+
|
|
114
|
+
# ======================================================
|
|
115
|
+
# PROPAGATION
|
|
116
|
+
# ======================================================
|
|
117
|
+
|
|
118
|
+
# Permite que pytest caplog capture logs
|
|
119
|
+
logger.propagate = True
|
|
120
|
+
|
|
121
|
+
return logger
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ==========================================================
|
|
125
|
+
# LOGGER PADRÃO DO MTCLI
|
|
126
|
+
# ==========================================================
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
Logger padrão utilizado por módulos internos do mtcli.
|
|
130
|
+
|
|
131
|
+
Plugins geralmente criam seu próprio logger usando:
|
|
132
|
+
|
|
133
|
+
setup_logger(__name__)
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
log = setup_logger()
|
|
@@ -1,24 +1,38 @@
|
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
|
|
18
|
+
for t in ticks:
|
|
19
|
+
self.buffer.append(t)
|
|
20
|
+
|
|
21
|
+
def add(self, tick):
|
|
22
|
+
|
|
23
|
+
self.buffer.append(tick)
|
|
24
|
+
|
|
25
|
+
def get_all(self):
|
|
26
|
+
|
|
27
|
+
return list(self.buffer)
|
|
28
|
+
|
|
29
|
+
def get_last(self):
|
|
30
|
+
|
|
31
|
+
if self.buffer:
|
|
32
|
+
return self.buffer[-1]
|
|
33
|
+
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
def clear(self):
|
|
37
|
+
|
|
38
|
+
self.buffer.clear()
|