mtcli-range 1.0.0.dev1__tar.gz → 1.0.0.dev3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mtcli-range
3
- Version: 1.0.0.dev1
3
+ Version: 1.0.0.dev3
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 (>=3.2.0)
22
+ Requires-Dist: mtcli (>=3.6.1,<4.0)
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
- Permite definir valores padrão para:
6
- - símbolo
7
- - tamanho do range
8
- - timeframe base
9
- - quantidade de candles
5
+ Também 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
- O Range é baseado em movimentação de preço fixa (range_size).
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 gerar_range(self) -> List[RangeBlock]:
61
+ def _obter_preco_ancora(self, rates):
62
62
  """
63
- Constrói blocos de Range a partir dos candles base.
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 inicial
94
+ # 🔹 Âncora
74
95
  if self.ancorar_abertura:
75
- ultimo_preco_base = rates[0]["open"]
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 a partir do fechamento do primeiro candle.")
99
+ log.info("Range iniciado pelo primeiro fechamento.")
80
100
 
81
- for candle in rates[1:]:
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
- bloco = RangeBlock(
94
- open=ultimo_preco_base,
95
- close=novo_close,
96
- direction=direcao
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.dev1"
3
+ version = "1.0.0.dev3"
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.2.0",
36
+ "mtcli>=3.6.1,<4.0",
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
- )