mtcli 3.7.2.dev0__tar.gz → 3.7.3.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.2.dev0 → mtcli-3.7.3.dev0}/PKG-INFO +2 -1
- mtcli-3.7.3.dev0/mtcli/__main__.py +3 -0
- mtcli-3.7.3.dev0/mtcli/cli.py +51 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/conf.py +30 -1
- mtcli-3.7.3.dev0/mtcli/logger.py +136 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/utils/pidfile.py +35 -35
- mtcli-3.7.3.dev0/mtcli/utils/time.py +65 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/pyproject.toml +3 -1
- mtcli-3.7.2.dev0/mtcli/cli.py +0 -74
- mtcli-3.7.2.dev0/mtcli/database.py +0 -54
- mtcli-3.7.2.dev0/mtcli/logger.py +0 -178
- mtcli-3.7.2.dev0/mtcli/marketdata/tick_cache.py +0 -24
- mtcli-3.7.2.dev0/mtcli/marketdata/tick_repository.py +0 -142
- mtcli-3.7.2.dev0/mtcli/utils/__init__.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/LICENSE +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/README.md +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/__init__.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/commands/__init__.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/commands/bars.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/commands/conf.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/commands/doctor.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/conecta.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/config_registre.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/data/__init__.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/data/base.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/data/csv.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/data/mt5.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/domain/__init__.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/domain/timeframe.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/models/__init__.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/models/bar_model.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/models/bars_model.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/models/chart_model.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/models/conf_model.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/models/consecutive_bars_model.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/models/rates_model.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/models/signals_model.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/models/unconsecutive_bar_model.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/mt5_context.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugin.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugin_loader.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugin_manager.py +0 -0
- {mtcli-3.7.2.dev0/mtcli/marketdata → mtcli-3.7.3.dev0/mtcli/plugins}/__init__.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/exemplo.py-dist +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/media_movel/__init__.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/media_movel/cli.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/media_movel/conf.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/media_movel/models/__init__.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/media_movel/models/model_media_movel.py +0 -0
- {mtcli-3.7.2.dev0/mtcli/plugins → mtcli-3.7.3.dev0/mtcli/plugins/media_movel/tests}/__init__.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/media_movel/tests/test_mm.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/media_movel/tests/test_model_media_movel.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/range_medio/__init__.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/range_medio/cli.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/range_medio/conf.py +0 -0
- {mtcli-3.7.2.dev0/mtcli/plugins/media_movel/tests → mtcli-3.7.3.dev0/mtcli/plugins/range_medio/models}/__init__.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/range_medio/models/average_range_model.py +0 -0
- {mtcli-3.7.2.dev0/mtcli/plugins/range_medio/models → mtcli-3.7.3.dev0/mtcli/plugins/range_medio/tests}/__init__.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/range_medio/tests/test_rm.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/volume_medio/__init__.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/volume_medio/cli.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/volume_medio/conf.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/volume_medio/models/__init__.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/volume_medio/models/model_average_volume.py +0 -0
- {mtcli-3.7.2.dev0/mtcli/plugins/range_medio → mtcli-3.7.3.dev0/mtcli/plugins/volume_medio}/tests/__init__.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/volume_medio/tests/test_vm.py +0 -0
- {mtcli-3.7.2.dev0/mtcli/plugins/volume_medio/tests → mtcli-3.7.3.dev0/mtcli/utils}/__init__.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/views/__init__.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/views/close_view.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/views/full_view.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/views/high_view.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/views/low_view.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/views/min_view.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/views/open_view.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/views/ranges_view.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/views/rates_view.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/views/vars_view.py +0 -0
- {mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/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.
|
|
3
|
+
Version: 3.7.3.dev0
|
|
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,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
|
-
#
|
|
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()
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import sys
|
|
3
|
-
import signal
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class PidFile:
|
|
7
|
-
|
|
8
|
-
def __init__(self, path: str):
|
|
9
|
-
self.path = path
|
|
10
|
-
|
|
11
|
-
def create(self):
|
|
12
|
-
|
|
13
|
-
if os.path.exists(self.path):
|
|
14
|
-
|
|
15
|
-
with open(self.path) as f:
|
|
16
|
-
pid = int(f.read().strip())
|
|
17
|
-
|
|
18
|
-
if self._pid_running(pid):
|
|
19
|
-
print(f"Monitor já está rodando (PID {pid})")
|
|
20
|
-
sys.exit(1)
|
|
21
|
-
|
|
22
|
-
with open(self.path, "w") as f:
|
|
23
|
-
f.write(str(os.getpid()))
|
|
24
|
-
|
|
25
|
-
def remove(self):
|
|
26
|
-
if os.path.exists(self.path):
|
|
27
|
-
os.remove(self.path)
|
|
28
|
-
|
|
29
|
-
def _pid_running(self, pid: int):
|
|
30
|
-
|
|
31
|
-
try:
|
|
32
|
-
os.kill(pid, 0)
|
|
33
|
-
return True
|
|
34
|
-
except OSError:
|
|
35
|
-
return False
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import signal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PidFile:
|
|
7
|
+
|
|
8
|
+
def __init__(self, path: str):
|
|
9
|
+
self.path = path
|
|
10
|
+
|
|
11
|
+
def create(self):
|
|
12
|
+
|
|
13
|
+
if os.path.exists(self.path):
|
|
14
|
+
|
|
15
|
+
with open(self.path) as f:
|
|
16
|
+
pid = int(f.read().strip())
|
|
17
|
+
|
|
18
|
+
if self._pid_running(pid):
|
|
19
|
+
print(f"Monitor já está rodando (PID {pid})")
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
|
|
22
|
+
with open(self.path, "w") as f:
|
|
23
|
+
f.write(str(os.getpid()))
|
|
24
|
+
|
|
25
|
+
def remove(self):
|
|
26
|
+
if os.path.exists(self.path):
|
|
27
|
+
os.remove(self.path)
|
|
28
|
+
|
|
29
|
+
def _pid_running(self, pid: int):
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
os.kill(pid, 0)
|
|
33
|
+
return True
|
|
34
|
+
except OSError:
|
|
35
|
+
return False
|
|
@@ -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.
|
|
3
|
+
version = "3.7.3.dev0"
|
|
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.dev0/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()
|
mtcli-3.7.2.dev0/mtcli/logger.py
DELETED
|
@@ -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
|
{mtcli-3.7.2.dev0/mtcli/plugins → mtcli-3.7.3.dev0/mtcli/plugins/media_movel/tests}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/media_movel/tests/test_model_media_movel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/range_medio/models/average_range_model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mtcli-3.7.2.dev0 → mtcli-3.7.3.dev0}/mtcli/plugins/volume_medio/models/model_average_volume.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mtcli-3.7.2.dev0/mtcli/plugins/volume_medio/tests → mtcli-3.7.3.dev0/mtcli/utils}/__init__.py
RENAMED
|
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
|