mtcli 3.6.1__tar.gz → 3.7.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.6.1 → mtcli-3.7.0.dev0}/PKG-INFO +1 -1
- mtcli-3.7.0.dev0/mtcli/commands/conf.py +44 -0
- mtcli-3.7.0.dev0/mtcli/conf.py +233 -0
- mtcli-3.7.0.dev0/mtcli/config_registre.py +41 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/logger.py +136 -136
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/models/rates_model.py +41 -41
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugin.py +18 -18
- mtcli-3.7.0.dev0/mtcli/plugin_loader.py +81 -0
- mtcli-3.7.0.dev0/mtcli/plugin_manager.py +27 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/pyproject.toml +1 -1
- mtcli-3.6.1/mtcli/conf.py +0 -199
- mtcli-3.6.1/mtcli/plugin_loader.py +0 -97
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/LICENSE +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/README.md +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/__init__.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/cli.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/commands/__init__.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/commands/bars.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/conecta.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/data/__init__.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/data/base.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/data/csv.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/data/mt5.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/database.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/domain/__init__.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/domain/timeframe.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/marketdata/__init__.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/marketdata/tick_cache.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/marketdata/tick_repository.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/models/__init__.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/models/bar_model.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/models/bars_model.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/models/chart_model.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/models/conf_model.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/models/consecutive_bars_model.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/models/signals_model.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/models/unconsecutive_bar_model.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/mt5_context.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/__init__.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/exemplo.py-dist +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/media_movel/__init__.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/media_movel/cli.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/media_movel/conf.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/media_movel/models/__init__.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/media_movel/models/model_media_movel.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/media_movel/tests/__init__.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/media_movel/tests/test_mm.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/media_movel/tests/test_model_media_movel.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/range_medio/__init__.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/range_medio/cli.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/range_medio/conf.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/range_medio/models/__init__.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/range_medio/models/average_range_model.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/range_medio/tests/__init__.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/range_medio/tests/test_rm.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/volume_medio/__init__.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/volume_medio/cli.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/volume_medio/conf.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/volume_medio/models/__init__.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/volume_medio/models/model_average_volume.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/volume_medio/tests/__init__.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/plugins/volume_medio/tests/test_vm.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/views/__init__.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/views/close_view.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/views/full_view.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/views/high_view.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/views/low_view.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/views/min_view.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/views/open_view.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/views/ranges_view.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/views/rates_view.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/views/vars_view.py +0 -0
- {mtcli-3.6.1 → mtcli-3.7.0.dev0}/mtcli/views/volumes_view.py +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comando para exibir configurações disponíveis.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from mtcli.config_registry import registry
|
|
8
|
+
from mtcli.conf import conf
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command()
|
|
12
|
+
@click.argument("section", required=False)
|
|
13
|
+
def conf_cmd(section):
|
|
14
|
+
"""
|
|
15
|
+
Exibe configurações disponíveis.
|
|
16
|
+
|
|
17
|
+
Exemplos:
|
|
18
|
+
|
|
19
|
+
mt conf
|
|
20
|
+
mt conf renko
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
if section:
|
|
24
|
+
options = registry.get_section(section)
|
|
25
|
+
else:
|
|
26
|
+
options = registry.get_all()
|
|
27
|
+
|
|
28
|
+
if not options:
|
|
29
|
+
click.echo("Nenhuma configuração registrada.")
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
for opt in options:
|
|
33
|
+
|
|
34
|
+
value = conf.get(opt.name, section=opt.section, default=opt.default)
|
|
35
|
+
|
|
36
|
+
click.echo(
|
|
37
|
+
f"{opt.section}.{opt.name} = {value} "
|
|
38
|
+
f"(default={opt.default})"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if opt.description:
|
|
42
|
+
click.echo(f" {opt.description}")
|
|
43
|
+
|
|
44
|
+
click.echo()
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sistema central de configuração do mtcli.
|
|
3
|
+
|
|
4
|
+
Fornece leitura de configuração a partir de:
|
|
5
|
+
|
|
6
|
+
1. Variáveis de ambiente
|
|
7
|
+
2. Arquivo mtcli.ini
|
|
8
|
+
3. Valores default
|
|
9
|
+
|
|
10
|
+
Também oferece utilidades usadas por plugins como:
|
|
11
|
+
|
|
12
|
+
- descoberta do diretório MQL5/Files
|
|
13
|
+
- seleção da fonte de dados (CSV ou MT5)
|
|
14
|
+
|
|
15
|
+
Plugins devem acessar a configuração através do objeto global `conf`.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import configparser
|
|
20
|
+
|
|
21
|
+
import MetaTrader5 as mt5
|
|
22
|
+
|
|
23
|
+
from mtcli.mt5_context import mt5_conexao
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Config:
|
|
27
|
+
"""
|
|
28
|
+
Gerenciador central de configurações do mtcli.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, filename="mtcli.ini"):
|
|
32
|
+
self.config = configparser.ConfigParser()
|
|
33
|
+
self.config.read(filename)
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------
|
|
36
|
+
# leitura de valores
|
|
37
|
+
# ---------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def get(self, key, section="DEFAULT", cast=None, default=None):
|
|
40
|
+
"""
|
|
41
|
+
Retorna um valor de configuração.
|
|
42
|
+
|
|
43
|
+
Prioridade:
|
|
44
|
+
|
|
45
|
+
1. Variável de ambiente SECTION_KEY
|
|
46
|
+
2. Variável de ambiente KEY
|
|
47
|
+
3. mtcli.ini [section]
|
|
48
|
+
4. mtcli.ini [DEFAULT]
|
|
49
|
+
5. default
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
key (str)
|
|
53
|
+
section (str)
|
|
54
|
+
cast (type | None)
|
|
55
|
+
default (Any)
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Any
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
env_key = f"{section.upper()}_{key.upper()}"
|
|
62
|
+
|
|
63
|
+
value = os.getenv(env_key) or os.getenv(key.upper())
|
|
64
|
+
|
|
65
|
+
if value is None:
|
|
66
|
+
|
|
67
|
+
if self.config.has_option(section, key):
|
|
68
|
+
value = self.config.get(section, key)
|
|
69
|
+
|
|
70
|
+
elif self.config.has_option("DEFAULT", key):
|
|
71
|
+
value = self.config.get("DEFAULT", key)
|
|
72
|
+
|
|
73
|
+
else:
|
|
74
|
+
value = default
|
|
75
|
+
|
|
76
|
+
if cast and value is not None:
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
|
|
80
|
+
if cast is bool:
|
|
81
|
+
value = str(value).lower() in ("1", "true", "yes")
|
|
82
|
+
|
|
83
|
+
else:
|
|
84
|
+
value = cast(value)
|
|
85
|
+
|
|
86
|
+
except ValueError:
|
|
87
|
+
value = default
|
|
88
|
+
|
|
89
|
+
return value
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------
|
|
92
|
+
# seção helper
|
|
93
|
+
# ---------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
def section(self, section):
|
|
96
|
+
"""
|
|
97
|
+
Retorna um helper para acessar uma seção específica.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
class Section:
|
|
101
|
+
|
|
102
|
+
def __init__(self, parent, section):
|
|
103
|
+
self.parent = parent
|
|
104
|
+
self.section = section
|
|
105
|
+
|
|
106
|
+
def get(self, key, cast=None, default=None):
|
|
107
|
+
return self.parent.get(key, self.section, cast, default)
|
|
108
|
+
|
|
109
|
+
return Section(self, section)
|
|
110
|
+
|
|
111
|
+
# ---------------------------------------------------------
|
|
112
|
+
# caminho MT5
|
|
113
|
+
# ---------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
def get_csv_path(self):
|
|
116
|
+
"""
|
|
117
|
+
Retorna o caminho da pasta MQL5/Files.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
path = self.get("mt5_pasta")
|
|
121
|
+
|
|
122
|
+
if path:
|
|
123
|
+
return os.path.normpath(path) + os.sep
|
|
124
|
+
|
|
125
|
+
with mt5_conexao():
|
|
126
|
+
|
|
127
|
+
info = mt5.terminal_info()
|
|
128
|
+
|
|
129
|
+
if info is None:
|
|
130
|
+
raise RuntimeError(
|
|
131
|
+
"Não foi possível obter informações do terminal MT5."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
path = os.path.join(info.data_path, "MQL5", "Files")
|
|
135
|
+
|
|
136
|
+
return os.path.normpath(path) + os.sep
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------
|
|
139
|
+
# data source
|
|
140
|
+
# ---------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
def get_data_source(self, source=None):
|
|
143
|
+
"""
|
|
144
|
+
Retorna a fonte de dados configurada.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
from mtcli.data import CsvDataSource, MT5DataSource
|
|
148
|
+
|
|
149
|
+
src = (source or self.get("dados", default="mt5")).lower()
|
|
150
|
+
|
|
151
|
+
if src == "csv":
|
|
152
|
+
return CsvDataSource()
|
|
153
|
+
|
|
154
|
+
if src == "mt5":
|
|
155
|
+
return MT5DataSource()
|
|
156
|
+
|
|
157
|
+
raise ValueError(f"Fonte de dados desconhecida: {src}")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# instância global usada por todo o sistema
|
|
161
|
+
conf = Config()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ---------------------------------------------------------
|
|
165
|
+
# timeframes suportados
|
|
166
|
+
# ---------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
_HOURS = [12, 8, 6, 4, 3, 2, 1]
|
|
169
|
+
_MINUTES = [30, 20, 15, 12, 10, 6, 5, 4, 3, 2, 1]
|
|
170
|
+
|
|
171
|
+
TIMEFRAMES = (
|
|
172
|
+
["mn1", "w1", "d1"]
|
|
173
|
+
+ [f"h{i}" for i in _HOURS]
|
|
174
|
+
+ [f"m{i}" for i in _MINUTES]
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------
|
|
179
|
+
# Configurações gerais
|
|
180
|
+
# ---------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
SYMBOL = conf.get("symbol", default="WIN$N")
|
|
183
|
+
DIGITOS = conf.get("digitos", cast=int, default=2)
|
|
184
|
+
PERIOD = conf.get("period", default="D1")
|
|
185
|
+
BARS = conf.get("count", cast=int, default=999)
|
|
186
|
+
|
|
187
|
+
VIEW = conf.get("view", default="ch")
|
|
188
|
+
VOLUME = conf.get("volume", default="tick")
|
|
189
|
+
DATE = conf.get("date", default="")
|
|
190
|
+
|
|
191
|
+
# ---------------------------------------------------------
|
|
192
|
+
# Configurações de leitura de candles
|
|
193
|
+
# ---------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
LATERAL = conf.get("lateral", default="doji")
|
|
196
|
+
ALTA = conf.get("alta", default="verde")
|
|
197
|
+
BAIXA = conf.get("baixa", default="vermelho")
|
|
198
|
+
|
|
199
|
+
ROMPIMENTO_ALTA = conf.get("rompimento_alta", default="c")
|
|
200
|
+
ROMPIMENTO_BAIXA = conf.get("rompimento_baixa", default="v")
|
|
201
|
+
|
|
202
|
+
PERCENTUAL_ROMPIMENTO = conf.get(
|
|
203
|
+
"percentual_rompimento", cast=int, default=50
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
PERCENTUAL_DOJI = conf.get(
|
|
207
|
+
"percentual_doji", cast=int, default=10
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# ---------------------------------------------------------
|
|
211
|
+
# Configurações de padrões de barra
|
|
212
|
+
# ---------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
UP_BAR = conf.get("up_bar", default="asc")
|
|
215
|
+
DOWN_BAR = conf.get("down_bar", default="desc")
|
|
216
|
+
|
|
217
|
+
INSIDE_BAR = conf.get("inside_bar", default="ib")
|
|
218
|
+
OUTSIDE_BAR = conf.get("outside_bar", default="ob")
|
|
219
|
+
|
|
220
|
+
SOMBRA_SUPERIOR = conf.get("sombra_superior", default="top")
|
|
221
|
+
SOMBRA_INFERIOR = conf.get("sombra_inferior", default="bottom")
|
|
222
|
+
|
|
223
|
+
# ---------------------------------------------------------
|
|
224
|
+
# Fonte de dados
|
|
225
|
+
# ---------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
DATA_SOURCE = conf.get_data_source()
|
|
228
|
+
|
|
229
|
+
# ---------------------------------------------------------
|
|
230
|
+
# caminho inicial do CSV (pode vir do ini/env)
|
|
231
|
+
# ---------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
_INITIAL_CSV_PATH = conf.get_csv_path()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Registry de configurações do mtcli.
|
|
3
|
+
|
|
4
|
+
Plugins podem registrar suas opções de configuração aqui
|
|
5
|
+
para permitir descoberta automática via CLI.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConfigOption:
|
|
10
|
+
"""
|
|
11
|
+
Representa uma opção de configuração registrada.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, section, name, type=str, default=None, description=""):
|
|
15
|
+
self.section = section
|
|
16
|
+
self.name = name
|
|
17
|
+
self.type = type
|
|
18
|
+
self.default = default
|
|
19
|
+
self.description = description
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConfigRegistry:
|
|
23
|
+
"""
|
|
24
|
+
Registro central de opções de configuração.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
self._options = []
|
|
29
|
+
|
|
30
|
+
def register(self, section, name, type=str, default=None, description=""):
|
|
31
|
+
option = ConfigOption(section, name, type, default, description)
|
|
32
|
+
self._options.append(option)
|
|
33
|
+
|
|
34
|
+
def get_all(self):
|
|
35
|
+
return self._options
|
|
36
|
+
|
|
37
|
+
def get_section(self, section):
|
|
38
|
+
return [o for o in self._options if o.section == section]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
registry = ConfigRegistry()
|
|
@@ -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,41 +1,41 @@
|
|
|
1
|
-
"""Módulo do model para obtenção das cotações."""
|
|
2
|
-
|
|
3
|
-
from datetime import datetime
|
|
4
|
-
|
|
5
|
-
from mtcli import conf
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class RatesModel:
|
|
9
|
-
"""Classe do model para obtenção das cotações."""
|
|
10
|
-
|
|
11
|
-
def __init__(self, symbol, period, count=999, start=None, end=None, limit=None):
|
|
12
|
-
"""Construtor da classe model rates."""
|
|
13
|
-
self.symbol = symbol
|
|
14
|
-
self.period = period
|
|
15
|
-
self.count = count
|
|
16
|
-
self.start = start
|
|
17
|
-
self.end = end
|
|
18
|
-
self.limit = limit
|
|
19
|
-
self.source = conf.get_data_source()
|
|
20
|
-
|
|
21
|
-
def get_data(self):
|
|
22
|
-
"""Obtém a lista das cotações com filtros opcionais."""
|
|
23
|
-
data = self.source.get_data(self.symbol, self.period, self.count)
|
|
24
|
-
|
|
25
|
-
# Filtro por data
|
|
26
|
-
if self.start or self.end:
|
|
27
|
-
|
|
28
|
-
def filtrar(linha):
|
|
29
|
-
datahora = datetime.strptime(linha[0], "%Y.%m.%d %H:%M:%S")
|
|
30
|
-
if self.start and datahora < self.start:
|
|
31
|
-
return False
|
|
32
|
-
if self.end and datahora > self.end:
|
|
33
|
-
return False
|
|
34
|
-
return True
|
|
35
|
-
|
|
36
|
-
data = list(filter(filtrar, data))
|
|
37
|
-
# Limite de linhas
|
|
38
|
-
if self.limit:
|
|
39
|
-
data = data[-self.limit :]
|
|
40
|
-
|
|
41
|
-
return data
|
|
1
|
+
"""Módulo do model para obtenção das cotações."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from mtcli.conf import conf
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RatesModel:
|
|
9
|
+
"""Classe do model para obtenção das cotações."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, symbol, period, count=999, start=None, end=None, limit=None):
|
|
12
|
+
"""Construtor da classe model rates."""
|
|
13
|
+
self.symbol = symbol
|
|
14
|
+
self.period = period
|
|
15
|
+
self.count = count
|
|
16
|
+
self.start = start
|
|
17
|
+
self.end = end
|
|
18
|
+
self.limit = limit
|
|
19
|
+
self.source = conf.get_data_source()
|
|
20
|
+
|
|
21
|
+
def get_data(self):
|
|
22
|
+
"""Obtém a lista das cotações com filtros opcionais."""
|
|
23
|
+
data = self.source.get_data(self.symbol, self.period, self.count)
|
|
24
|
+
|
|
25
|
+
# Filtro por data
|
|
26
|
+
if self.start or self.end:
|
|
27
|
+
|
|
28
|
+
def filtrar(linha):
|
|
29
|
+
datahora = datetime.strptime(linha[0], "%Y.%m.%d %H:%M:%S")
|
|
30
|
+
if self.start and datahora < self.start:
|
|
31
|
+
return False
|
|
32
|
+
if self.end and datahora > self.end:
|
|
33
|
+
return False
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
data = list(filter(filtrar, data))
|
|
37
|
+
# Limite de linhas
|
|
38
|
+
if self.limit:
|
|
39
|
+
data = data[-self.limit :]
|
|
40
|
+
|
|
41
|
+
return data
|