mtcli-range 1.0.0.dev1__tar.gz → 1.0.0.dev2__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_range-1.0.0.dev1 → mtcli_range-1.0.0.dev2}/PKG-INFO +2 -2
- {mtcli_range-1.0.0.dev1 → mtcli_range-1.0.0.dev2}/mtcli_range/conf.py +17 -9
- mtcli_range-1.0.0.dev2/mtcli_range/range_controller.py +115 -0
- {mtcli_range-1.0.0.dev1 → mtcli_range-1.0.0.dev2}/mtcli_range/range_model.py +38 -17
- {mtcli_range-1.0.0.dev1 → mtcli_range-1.0.0.dev2}/pyproject.toml +2 -2
- mtcli_range-1.0.0.dev1/mtcli_range/range_controller.py +0 -65
- {mtcli_range-1.0.0.dev1 → mtcli_range-1.0.0.dev2}/LICENSE +0 -0
- {mtcli_range-1.0.0.dev1 → mtcli_range-1.0.0.dev2}/README.md +0 -0
- {mtcli_range-1.0.0.dev1 → mtcli_range-1.0.0.dev2}/mtcli_range/__init__.py +0 -0
- {mtcli_range-1.0.0.dev1 → mtcli_range-1.0.0.dev2}/mtcli_range/cli.py +0 -0
- {mtcli_range-1.0.0.dev1 → mtcli_range-1.0.0.dev2}/mtcli_range/plugin.py +0 -0
- {mtcli_range-1.0.0.dev1 → mtcli_range-1.0.0.dev2}/mtcli_range/range_view.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mtcli-range
|
|
3
|
-
Version: 1.0.0.
|
|
3
|
+
Version: 1.0.0.dev2
|
|
4
4
|
Summary: Range bars plugin para mtcli
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -19,7 +19,7 @@ Classifier: Operating System :: OS Independent
|
|
|
19
19
|
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
20
20
|
Requires-Dist: click (>=8.3.0,<9.0.0)
|
|
21
21
|
Requires-Dist: metatrader5 (>=5.0.5370,<6.0.0)
|
|
22
|
-
Requires-Dist: mtcli (
|
|
22
|
+
Requires-Dist: mtcli (==3.5.*)
|
|
23
23
|
Project-URL: Documentation, https://vfranca.github.io/mtcli-range
|
|
24
24
|
Project-URL: Homepage, https://github.com/vfranca/mtcli-range
|
|
25
25
|
Project-URL: Issues, https://github.com/vfranca/mtcli-range/issues
|
|
@@ -2,11 +2,7 @@
|
|
|
2
2
|
Configuração do plugin mtcli-range.
|
|
3
3
|
|
|
4
4
|
Lê parâmetros do arquivo mtcli.ini na seção [range].
|
|
5
|
-
|
|
6
|
-
- símbolo
|
|
7
|
-
- tamanho do range
|
|
8
|
-
- timeframe base
|
|
9
|
-
- quantidade de candles
|
|
5
|
+
Também lê session_open da seção [renko] ou [range].
|
|
10
6
|
"""
|
|
11
7
|
|
|
12
8
|
import configparser
|
|
@@ -22,20 +18,32 @@ SECTION = "range"
|
|
|
22
18
|
|
|
23
19
|
|
|
24
20
|
def get_default_symbol() -> str:
|
|
25
|
-
"""Retorna símbolo padrão do arquivo .ini."""
|
|
26
21
|
return config.get(SECTION, "symbol", fallback="WIN$")
|
|
27
22
|
|
|
28
23
|
|
|
29
24
|
def get_default_range() -> float:
|
|
30
|
-
"""Retorna tamanho do range padrão."""
|
|
31
25
|
return config.getfloat(SECTION, "range_size", fallback=50.0)
|
|
32
26
|
|
|
33
27
|
|
|
34
28
|
def get_default_timeframe() -> str:
|
|
35
|
-
"""Retorna timeframe base padrão."""
|
|
36
29
|
return config.get(SECTION, "timeframe", fallback="m5")
|
|
37
30
|
|
|
38
31
|
|
|
39
32
|
def get_default_bars() -> int:
|
|
40
|
-
"""Retorna quantidade padrão de candles base."""
|
|
41
33
|
return config.getint(SECTION, "bars", fallback=500)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_session_open() -> str | None:
|
|
37
|
+
"""
|
|
38
|
+
Retorna horário de abertura da sessão no formato HH:MM.
|
|
39
|
+
Busca:
|
|
40
|
+
1. [range]
|
|
41
|
+
2. [renko]
|
|
42
|
+
"""
|
|
43
|
+
if config.has_option("range", "session_open"):
|
|
44
|
+
return config.get("range", "session_open")
|
|
45
|
+
|
|
46
|
+
if config.has_option("renko", "session_open"):
|
|
47
|
+
return config.get("renko", "session_open")
|
|
48
|
+
|
|
49
|
+
return None
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Controller do plugin mtcli-range.
|
|
3
|
+
|
|
4
|
+
Responsável por orquestrar:
|
|
5
|
+
|
|
6
|
+
- Leitura de parâmetros da CLI
|
|
7
|
+
- Conversão de timeframe via Enum central
|
|
8
|
+
- Leitura de session_open do mtcli.ini
|
|
9
|
+
- Conexão segura com MT5
|
|
10
|
+
- Execução do Model
|
|
11
|
+
- Renderização da View
|
|
12
|
+
|
|
13
|
+
Mantém separação clara entre:
|
|
14
|
+
Model -> cálculo dos blocos
|
|
15
|
+
View -> exibição no terminal
|
|
16
|
+
Controller -> coordenação do fluxo
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from mtcli.mt5_context import mt5_conexao
|
|
20
|
+
from mtcli.logger import setup_logger
|
|
21
|
+
from mtcli.domain.timeframe import Timeframe
|
|
22
|
+
|
|
23
|
+
from .range_model import RangeModel
|
|
24
|
+
from .range_view import RangeView
|
|
25
|
+
from .conf import get_session_open
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
log = setup_logger("mtcli.range.controller")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RangeController:
|
|
32
|
+
"""
|
|
33
|
+
Controlador principal do gráfico Range.
|
|
34
|
+
|
|
35
|
+
Parâmetros:
|
|
36
|
+
symbol (str): ativo
|
|
37
|
+
timeframe (str): timeframe textual (ex: m5, h1)
|
|
38
|
+
bars (int): quantidade de candles base
|
|
39
|
+
range_size (float): tamanho fixo do range
|
|
40
|
+
ancorar_abertura (bool): se True, ancora na abertura da sessão
|
|
41
|
+
numerar (bool): se True, numera as linhas exibidas
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
symbol: str,
|
|
47
|
+
timeframe: str,
|
|
48
|
+
bars: int,
|
|
49
|
+
range_size: float,
|
|
50
|
+
ancorar_abertura: bool,
|
|
51
|
+
numerar: bool,
|
|
52
|
+
):
|
|
53
|
+
self.symbol = symbol
|
|
54
|
+
self.timeframe = Timeframe.from_string(timeframe)
|
|
55
|
+
self.bars = bars
|
|
56
|
+
self.range_size = range_size
|
|
57
|
+
self.ancorar_abertura = ancorar_abertura
|
|
58
|
+
self.numerar = numerar
|
|
59
|
+
|
|
60
|
+
def executar(self):
|
|
61
|
+
"""
|
|
62
|
+
Executa o fluxo completo:
|
|
63
|
+
|
|
64
|
+
1. Lê session_open do mtcli.ini
|
|
65
|
+
2. Abre conexão MT5
|
|
66
|
+
3. Instancia o Model
|
|
67
|
+
4. Gera blocos Range
|
|
68
|
+
5. Exibe resultado via View
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
log.info("Iniciando execução do mtcli-range.")
|
|
72
|
+
|
|
73
|
+
# 🔹 Lê horário de abertura de sessão do mtcli.ini
|
|
74
|
+
session_open = get_session_open()
|
|
75
|
+
|
|
76
|
+
if self.ancorar_abertura:
|
|
77
|
+
if session_open:
|
|
78
|
+
log.info(f"Ancoragem habilitada. session_open = {session_open}")
|
|
79
|
+
else:
|
|
80
|
+
log.warning(
|
|
81
|
+
"Ancoragem solicitada, mas session_open não encontrado no mtcli.ini. "
|
|
82
|
+
"Será usado o primeiro fechamento disponível."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
with mt5_conexao():
|
|
87
|
+
|
|
88
|
+
model = RangeModel(
|
|
89
|
+
symbol=self.symbol,
|
|
90
|
+
timeframe_const=self.timeframe.mt5_const,
|
|
91
|
+
bars=self.bars,
|
|
92
|
+
range_size=self.range_size,
|
|
93
|
+
ancorar_abertura=self.ancorar_abertura,
|
|
94
|
+
session_open=session_open,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
blocos = model.gerar_range()
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
log.error(f"Erro durante execução do RangeController: {e}")
|
|
101
|
+
raise
|
|
102
|
+
|
|
103
|
+
# 🔹 Exibição fora do contexto MT5
|
|
104
|
+
RangeView.exibir_header(
|
|
105
|
+
symbol=self.symbol,
|
|
106
|
+
range_size=self.range_size,
|
|
107
|
+
ancorado=self.ancorar_abertura,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
RangeView.exibir_blocos(
|
|
111
|
+
blocos=blocos,
|
|
112
|
+
numerar=self.numerar,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
log.info("Execução do mtcli-range finalizada com sucesso.")
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Model responsável pela geração dos blocos Range.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
Um novo bloco é criado somente quando o preço se move
|
|
6
|
-
range_size pontos acima ou abaixo do fechamento (ou abertura ancorada).
|
|
4
|
+
Implementa ancoragem de sessão baseada em session_open.
|
|
7
5
|
"""
|
|
8
6
|
|
|
9
7
|
from dataclasses import dataclass
|
|
10
8
|
from typing import List
|
|
11
9
|
import MetaTrader5 as mt5
|
|
10
|
+
from datetime import datetime, time
|
|
12
11
|
from mtcli.logger import setup_logger
|
|
13
12
|
|
|
14
13
|
log = setup_logger("mtcli.range.model")
|
|
@@ -16,7 +15,6 @@ log = setup_logger("mtcli.range.model")
|
|
|
16
15
|
|
|
17
16
|
@dataclass
|
|
18
17
|
class RangeBlock:
|
|
19
|
-
"""Representa um bloco de Range."""
|
|
20
18
|
open: float
|
|
21
19
|
close: float
|
|
22
20
|
direction: str # "UP" ou "DOWN"
|
|
@@ -27,6 +25,7 @@ class RangeModel:
|
|
|
27
25
|
Responsável por:
|
|
28
26
|
- Buscar dados no MT5
|
|
29
27
|
- Calcular blocos de Range
|
|
28
|
+
- Aplicar ancoragem por abertura de sessão
|
|
30
29
|
"""
|
|
31
30
|
|
|
32
31
|
def __init__(
|
|
@@ -36,15 +35,16 @@ class RangeModel:
|
|
|
36
35
|
bars: int,
|
|
37
36
|
range_size: float,
|
|
38
37
|
ancorar_abertura: bool = False,
|
|
38
|
+
session_open: str | None = None,
|
|
39
39
|
):
|
|
40
40
|
self.symbol = symbol
|
|
41
41
|
self.timeframe_const = timeframe_const
|
|
42
42
|
self.bars = bars
|
|
43
43
|
self.range_size = range_size
|
|
44
44
|
self.ancorar_abertura = ancorar_abertura
|
|
45
|
+
self.session_open = session_open
|
|
45
46
|
|
|
46
47
|
def obter_candles(self):
|
|
47
|
-
"""Obtém candles do MT5."""
|
|
48
48
|
log.info(f"Buscando {self.bars} candles de {self.symbol}")
|
|
49
49
|
rates = mt5.copy_rates_from_pos(
|
|
50
50
|
self.symbol,
|
|
@@ -58,11 +58,32 @@ class RangeModel:
|
|
|
58
58
|
|
|
59
59
|
return rates
|
|
60
60
|
|
|
61
|
-
def
|
|
61
|
+
def _obter_preco_ancora(self, rates):
|
|
62
62
|
"""
|
|
63
|
-
|
|
63
|
+
Retorna preço âncora baseado na abertura da sessão.
|
|
64
64
|
"""
|
|
65
65
|
|
|
66
|
+
if not self.session_open:
|
|
67
|
+
log.warning("session_open não configurado. Usando primeiro close.")
|
|
68
|
+
return rates[0]["close"]
|
|
69
|
+
|
|
70
|
+
hora_sessao = datetime.strptime(self.session_open, "%H:%M").time()
|
|
71
|
+
|
|
72
|
+
for candle in rates:
|
|
73
|
+
dt = datetime.fromtimestamp(candle["time"])
|
|
74
|
+
if dt.time() >= hora_sessao:
|
|
75
|
+
log.info(
|
|
76
|
+
f"Âncora encontrada na sessão: "
|
|
77
|
+
f"{dt.strftime('%Y-%m-%d %H:%M')} "
|
|
78
|
+
f"preço abertura {candle['open']}"
|
|
79
|
+
)
|
|
80
|
+
return candle["open"]
|
|
81
|
+
|
|
82
|
+
log.warning("Sessão não encontrada nos candles carregados.")
|
|
83
|
+
return rates[0]["close"]
|
|
84
|
+
|
|
85
|
+
def gerar_range(self) -> List[RangeBlock]:
|
|
86
|
+
|
|
66
87
|
rates = self.obter_candles()
|
|
67
88
|
|
|
68
89
|
if len(rates) == 0:
|
|
@@ -70,15 +91,14 @@ class RangeModel:
|
|
|
70
91
|
|
|
71
92
|
blocos: List[RangeBlock] = []
|
|
72
93
|
|
|
73
|
-
# 🔹 Âncora
|
|
94
|
+
# 🔹 Âncora
|
|
74
95
|
if self.ancorar_abertura:
|
|
75
|
-
ultimo_preco_base = rates
|
|
76
|
-
log.info("Range ancorado na abertura do primeiro candle.")
|
|
96
|
+
ultimo_preco_base = self._obter_preco_ancora(rates)
|
|
77
97
|
else:
|
|
78
98
|
ultimo_preco_base = rates[0]["close"]
|
|
79
|
-
log.info("Range iniciado
|
|
99
|
+
log.info("Range iniciado pelo primeiro fechamento.")
|
|
80
100
|
|
|
81
|
-
for candle in rates
|
|
101
|
+
for candle in rates:
|
|
82
102
|
preco = candle["close"]
|
|
83
103
|
|
|
84
104
|
while abs(preco - ultimo_preco_base) >= self.range_size:
|
|
@@ -90,13 +110,14 @@ class RangeModel:
|
|
|
90
110
|
novo_close = ultimo_preco_base - self.range_size
|
|
91
111
|
direcao = "DOWN"
|
|
92
112
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
113
|
+
blocos.append(
|
|
114
|
+
RangeBlock(
|
|
115
|
+
open=ultimo_preco_base,
|
|
116
|
+
close=novo_close,
|
|
117
|
+
direction=direcao
|
|
118
|
+
)
|
|
97
119
|
)
|
|
98
120
|
|
|
99
|
-
blocos.append(bloco)
|
|
100
121
|
ultimo_preco_base = novo_close
|
|
101
122
|
|
|
102
123
|
log.info(f"{len(blocos)} blocos de range gerados.")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mtcli-range"
|
|
3
|
-
version = "1.0.0.
|
|
3
|
+
version = "1.0.0.dev2"
|
|
4
4
|
description = "Range bars plugin para mtcli"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Valmir França", email = "vfranca3@gmail.com" }
|
|
@@ -33,7 +33,7 @@ classifiers = [
|
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
dependencies = [
|
|
36
|
-
"mtcli>=3.
|
|
36
|
+
"mtcli>=3.5.0.dev0,<3.6.0.dev0",
|
|
37
37
|
"click>=8.3.0,<9.0.0",
|
|
38
38
|
"metatrader5>=5.0.5370,<6.0.0"
|
|
39
39
|
]
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Controller do plugin mtcli-range.
|
|
3
|
-
|
|
4
|
-
Orquestra:
|
|
5
|
-
- Conexão MT5
|
|
6
|
-
- Model
|
|
7
|
-
- View
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from mtcli.mt5_context import mt5_conexao
|
|
11
|
-
from mtcli.logger import setup_logger
|
|
12
|
-
from mtcli.domain.timeframe import Timeframe
|
|
13
|
-
from .range_model import RangeModel
|
|
14
|
-
from .range_view import RangeView
|
|
15
|
-
|
|
16
|
-
log = setup_logger("mtcli.range.controller")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class RangeController:
|
|
20
|
-
"""
|
|
21
|
-
Controlador principal do fluxo.
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
def __init__(
|
|
25
|
-
self,
|
|
26
|
-
symbol: str,
|
|
27
|
-
timeframe: str,
|
|
28
|
-
bars: int,
|
|
29
|
-
range_size: float,
|
|
30
|
-
ancorar_abertura: bool,
|
|
31
|
-
numerar: bool,
|
|
32
|
-
):
|
|
33
|
-
self.symbol = symbol
|
|
34
|
-
self.timeframe = Timeframe.from_string(timeframe)
|
|
35
|
-
self.bars = bars
|
|
36
|
-
self.range_size = range_size
|
|
37
|
-
self.ancorar_abertura = ancorar_abertura
|
|
38
|
-
self.numerar = numerar
|
|
39
|
-
|
|
40
|
-
def executar(self):
|
|
41
|
-
"""
|
|
42
|
-
Executa o fluxo completo do gráfico Range.
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
with mt5_conexao():
|
|
46
|
-
model = RangeModel(
|
|
47
|
-
self.symbol,
|
|
48
|
-
self.timeframe.mt5_const,
|
|
49
|
-
self.bars,
|
|
50
|
-
self.range_size,
|
|
51
|
-
ancorar_abertura=self.ancorar_abertura,
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
blocos = model.gerar_range()
|
|
55
|
-
|
|
56
|
-
RangeView.exibir_header(
|
|
57
|
-
self.symbol,
|
|
58
|
-
self.range_size,
|
|
59
|
-
self.ancorar_abertura,
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
RangeView.exibir_blocos(
|
|
63
|
-
blocos,
|
|
64
|
-
numerar=self.numerar,
|
|
65
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|