mtcli-renko 1.1.0.dev2__tar.gz → 1.1.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.
Files changed (21) hide show
  1. {mtcli_renko-1.1.0.dev2 → mtcli_renko-1.1.0.dev3}/PKG-INFO +2 -2
  2. mtcli_renko-1.1.0.dev3/mtcli_renko/commands/renko.py +105 -0
  3. {mtcli_renko-1.1.0.dev2 → mtcli_renko-1.1.0.dev3}/mtcli_renko/conf.py +9 -0
  4. mtcli_renko-1.1.0.dev3/mtcli_renko/controllers/renko_controller.py +154 -0
  5. mtcli_renko-1.1.0.dev3/mtcli_renko/models/renko_model.py +315 -0
  6. mtcli_renko-1.1.0.dev3/mtcli_renko/views/renko_view.py +81 -0
  7. {mtcli_renko-1.1.0.dev2 → mtcli_renko-1.1.0.dev3}/pyproject.toml +3 -3
  8. mtcli_renko-1.1.0.dev2/mtcli_renko/commands/renko.py +0 -85
  9. mtcli_renko-1.1.0.dev2/mtcli_renko/controllers/renko_controller.py +0 -105
  10. mtcli_renko-1.1.0.dev2/mtcli_renko/domain/timeframe.py +0 -87
  11. mtcli_renko-1.1.0.dev2/mtcli_renko/models/renko_model.py +0 -235
  12. mtcli_renko-1.1.0.dev2/mtcli_renko/views/__init__.py +0 -0
  13. mtcli_renko-1.1.0.dev2/mtcli_renko/views/renko_view.py +0 -75
  14. {mtcli_renko-1.1.0.dev2 → mtcli_renko-1.1.0.dev3}/LICENSE +0 -0
  15. {mtcli_renko-1.1.0.dev2 → mtcli_renko-1.1.0.dev3}/README.md +0 -0
  16. {mtcli_renko-1.1.0.dev2 → mtcli_renko-1.1.0.dev3}/mtcli_renko/__init__.py +0 -0
  17. {mtcli_renko-1.1.0.dev2 → mtcli_renko-1.1.0.dev3}/mtcli_renko/commands/__init__.py +0 -0
  18. {mtcli_renko-1.1.0.dev2 → mtcli_renko-1.1.0.dev3}/mtcli_renko/controllers/__init__.py +0 -0
  19. {mtcli_renko-1.1.0.dev2/mtcli_renko/domain → mtcli_renko-1.1.0.dev3/mtcli_renko/models}/__init__.py +0 -0
  20. {mtcli_renko-1.1.0.dev2 → mtcli_renko-1.1.0.dev3}/mtcli_renko/plugin.py +0 -0
  21. {mtcli_renko-1.1.0.dev2/mtcli_renko/models → mtcli_renko-1.1.0.dev3/mtcli_renko/views}/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mtcli-renko
3
- Version: 1.1.0.dev2
3
+ Version: 1.1.0.dev3
4
4
  Summary: Renko plugin institucional para mtcli (MetaTrader 5)
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.5.0)
23
23
  Project-URL: Documentation, https://vfranca.github.io/mtcli-renko
24
24
  Project-URL: Homepage, https://github.com/vfranca/mtcli-renko
25
25
  Project-URL: Issues, https://github.com/vfranca/mtcli-renko/issues
@@ -0,0 +1,105 @@
1
+ """
2
+ Comando CLI para geração de gráfico Renko.
3
+ """
4
+
5
+ import click
6
+
7
+ from ..controllers.renko_controller import RenkoController
8
+ from ..views.renko_view import exibir_renko
9
+ from mtcli.domain.timeframe import Timeframe
10
+ from mtcli.logger import setup_logger
11
+ from ..conf import (
12
+ SYMBOL,
13
+ BRICK,
14
+ PERIOD,
15
+ BARS,
16
+ DATA_MODE,
17
+ MAX_TICKS,
18
+ TICK_STYLE,
19
+ MODO,
20
+ LIMIT_BRICKS
21
+ )
22
+
23
+ log = setup_logger(__name__)
24
+
25
+
26
+ @click.command()
27
+ @click.version_option(package_name="mtcli-renko")
28
+ @click.option("--symbol", "-s", default=SYMBOL, show_default=True)
29
+ @click.option("--brick", "-b", default=BRICK, show_default=True, type=float)
30
+ @click.option("--timeframe", "-t", default=PERIOD, show_default=True)
31
+ @click.option("--bars", "-n", default=BARS, show_default=True, type=int)
32
+ @click.option("--numerar/--no-numerar", default=False, show_default=True)
33
+ @click.option(
34
+ "--modo",
35
+ type=click.Choice(["simples", "classico"], case_sensitive=False),
36
+ default=MODO,
37
+ show_default=True,
38
+ help="Modo de calculo dos blocos"
39
+ )
40
+ @click.option("--ancorar-abertura", is_flag=True, show_default=True, help="Ancora na abertura do pregão")
41
+ @click.option(
42
+ "--data-mode",
43
+ type=click.Choice(["candle", "tick"]),
44
+ default=DATA_MODE,
45
+ show_default=True,
46
+ help="Dados baseados em candles ou ticks"
47
+ )
48
+ @click.option("--max-ticks", default=MAX_TICKS, type=int, show_default=True, help="Maximo de ticks usados no renko baseado em ticks")
49
+ @click.option(
50
+ "--tick-style",
51
+ type=click.Choice(["estrutural", "hibrido", "agressivo"]),
52
+ default=TICK_STYLE,
53
+ show_default=True,
54
+ help="Estilo de calculo dos blocos baseado em ticks"
55
+ )
56
+ @click.option("--limit-bricks", type=int, default=LIMIT_BRICKS, show_default=True, help="Limite de blocos")
57
+ @click.option("--price-min", type=float, default=None, show_default=True, help="Preço mínimo para filtrar blocos")
58
+ @click.option("--price-max", type=float, default=None, show_default=True, help="Preço máximo para filtrar blocos")
59
+ @click.option("--reverse", is_flag=True, show_default=True, help="Reverte a órdem dos blocos")
60
+
61
+
62
+ def renko(
63
+ symbol,
64
+ brick,
65
+ timeframe,
66
+ bars,
67
+ numerar,
68
+ modo,
69
+ ancorar_abertura,
70
+ data_mode,
71
+ max_ticks,
72
+ tick_style,
73
+ limit_bricks,
74
+ price_min,
75
+ price_max,
76
+ reverse,
77
+ ):
78
+ """
79
+ Gera gráfico Renko no terminal.
80
+ """
81
+
82
+ try:
83
+ tf_enum = Timeframe.from_string(timeframe)
84
+ except ValueError as e:
85
+ raise click.BadParameter(str(e))
86
+
87
+ controller = RenkoController(
88
+ symbol=symbol,
89
+ brick_size=brick,
90
+ timeframe=tf_enum.mt5_const,
91
+ quantidade=bars,
92
+ modo=modo,
93
+ ancorar_abertura=ancorar_abertura,
94
+ data_mode=data_mode,
95
+ max_ticks=max_ticks,
96
+ tick_style=tick_style,
97
+ price_min=price_min,
98
+ price_max=price_max,
99
+ limit_bricks=limit_bricks,
100
+ reverse=reverse,
101
+ )
102
+
103
+ resultado = controller.executar()
104
+
105
+ exibir_renko(resultado, numerar=numerar)
@@ -92,3 +92,12 @@ TICK_STYLE = os.getenv(
92
92
  _get_config_value("RENKO", "tick_style", "hibrido")
93
93
  )
94
94
 
95
+ MODO = os.getenv(
96
+ "MODO",
97
+ _get_config_value("RENKO", "modo", "simples")
98
+ )
99
+
100
+ LIMIT_BRICKS = int(os.getenv(
101
+ "LIMIT_BRICKS",
102
+ _get_config_value("RENKO", "limit_bricks", 0)
103
+ ))
@@ -0,0 +1,154 @@
1
+ """
2
+ Renko controller.
3
+
4
+ Responsável por:
5
+
6
+ - Orquestrar obtenção de dados (candle ou tick)
7
+ - Chamar o model
8
+ - Aplicar filtros e estilos
9
+ """
10
+
11
+ from ..models.renko_model import RenkoModel
12
+ from mtcli.logger import setup_logger
13
+
14
+ log = setup_logger(__name__)
15
+
16
+
17
+ class RenkoController:
18
+ """
19
+ Controller principal do Renko.
20
+
21
+ :param symbol: ativo
22
+ :param brick_size: tamanho do brick
23
+ :param timeframe: timeframe MT5
24
+ :param quantidade: número de candles
25
+ :param modo: simples | classico
26
+ :param ancorar_abertura: ancora sessão
27
+ :param data_mode: candle | tick
28
+ :param max_ticks: limite ticks
29
+ :param tick_style: estrutural | hibrido | agressivo
30
+ :param price_min: filtro preço mínimo
31
+ :param price_max: filtro preço máximo
32
+ :param limit_bricks: limite de blocos
33
+ :param reverse: inverter ordem
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ symbol,
39
+ brick_size,
40
+ timeframe,
41
+ quantidade,
42
+ modo="simples",
43
+ ancorar_abertura=False,
44
+ data_mode="candle",
45
+ max_ticks=3000,
46
+ tick_style="hibrido",
47
+ price_min=None,
48
+ price_max=None,
49
+ limit_bricks=None,
50
+ reverse=False,
51
+ ):
52
+
53
+ self.model = RenkoModel(symbol, brick_size)
54
+
55
+ self.timeframe = timeframe
56
+ self.quantidade = quantidade
57
+ self.modo = modo
58
+
59
+ self.ancorar_abertura = ancorar_abertura
60
+ self.data_mode = data_mode
61
+ self.max_ticks = max_ticks
62
+ self.tick_style = tick_style
63
+
64
+ self.price_min = price_min
65
+ self.price_max = price_max
66
+ self.limit_bricks = limit_bricks
67
+ self.reverse = reverse
68
+
69
+ # ==========================================================
70
+ # EXECUÇÃO
71
+ # ==========================================================
72
+
73
+ def executar(self):
74
+
75
+ # ======================================================
76
+ # TICK MODE
77
+ # ======================================================
78
+
79
+ if self.data_mode == "tick":
80
+
81
+ ticks = self.model.obter_ticks(
82
+ max_ticks=self.max_ticks,
83
+ ancorar_abertura=self.ancorar_abertura,
84
+ )
85
+
86
+ if ticks is None or len(ticks) == 0:
87
+ log.warning("Nenhum tick retornado.")
88
+ return []
89
+
90
+ resultado = self.model.construir_renko_ticks(ticks)
91
+
92
+ bricks = resultado.confirmados
93
+
94
+ # ======================================================
95
+ # CANDLE MODE
96
+ # ======================================================
97
+
98
+ else:
99
+
100
+ rates = self.model.obter_rates(
101
+ self.timeframe,
102
+ self.quantidade,
103
+ ancorar_abertura=self.ancorar_abertura,
104
+ )
105
+
106
+ if rates is None or len(rates) == 0:
107
+ log.warning("Nenhum candle retornado.")
108
+ return []
109
+
110
+ bricks = self.model.construir_renko(
111
+ rates,
112
+ modo=self.modo,
113
+ )
114
+
115
+ resultado = bricks
116
+
117
+ # ======================================================
118
+ # FILTROS
119
+ # ======================================================
120
+
121
+ if self.price_min is not None:
122
+ bricks = [b for b in bricks if b.close >= self.price_min]
123
+
124
+ if self.price_max is not None:
125
+ bricks = [b for b in bricks if b.close <= self.price_max]
126
+
127
+ if self.reverse:
128
+ bricks = list(reversed(bricks))
129
+
130
+ if self.limit_bricks:
131
+ bricks = bricks[-self.limit_bricks:]
132
+
133
+ # ======================================================
134
+ # TICK STYLE
135
+ # ======================================================
136
+
137
+ if self.data_mode == "tick":
138
+
139
+ if self.tick_style == "estrutural":
140
+ return bricks
141
+
142
+ if self.tick_style == "agressivo":
143
+
144
+ if resultado.em_formacao:
145
+ bricks.append(resultado.em_formacao)
146
+
147
+ return bricks
148
+
149
+ # híbrido
150
+ resultado = resultado._replace(confirmados=bricks)
151
+
152
+ return resultado
153
+
154
+ return bricks
@@ -0,0 +1,315 @@
1
+ """
2
+ RenkoModel profissional.
3
+
4
+ ✔ Candle mode determinístico
5
+ ✔ Tick mode híbrido (confirmados + em formação)
6
+ ✔ Compatível com controller atual
7
+ ✔ Funciona mesmo com mercado fechado
8
+ ✔ Correção numpy truth value
9
+ ✔ Correção timezone sessão
10
+ ✔ Path reconstruction institucional
11
+ """
12
+
13
+ from dataclasses import dataclass
14
+ from typing import List, Optional, NamedTuple
15
+ from zoneinfo import ZoneInfo
16
+ from datetime import datetime, time as dtime
17
+
18
+ import MetaTrader5 as mt5
19
+
20
+ from mtcli.mt5_context import mt5_conexao
21
+ from mtcli.logger import setup_logger
22
+ from mtcli.marketdata.tick_repository import TickRepository
23
+ from ..conf import SESSION_OPEN
24
+
25
+ log = setup_logger(__name__)
26
+
27
+
28
+ # ==========================================================
29
+ # DATA STRUCTURES
30
+ # ==========================================================
31
+
32
+ @dataclass
33
+ class Brick:
34
+ direction: str
35
+ open: float
36
+ close: float
37
+
38
+
39
+ class RenkoTickResult(NamedTuple):
40
+ confirmados: List[Brick]
41
+ em_formacao: Optional[Brick]
42
+
43
+
44
+ # ==========================================================
45
+ # MODEL
46
+ # ==========================================================
47
+
48
+ class RenkoModel:
49
+
50
+ def __init__(self, symbol: str, brick_size: float):
51
+
52
+ self.symbol = symbol
53
+ self.brick_size = brick_size
54
+ self.repo = TickRepository()
55
+
56
+ # ======================================================
57
+ # AUXILIAR
58
+ # ======================================================
59
+
60
+ def _ultimo_pregao_data(self, timeframe):
61
+
62
+ with mt5_conexao():
63
+
64
+ ultimo = mt5.copy_rates_from_pos(
65
+ self.symbol,
66
+ timeframe,
67
+ 0,
68
+ 1,
69
+ )
70
+
71
+ if ultimo is None or len(ultimo) == 0:
72
+ return None
73
+
74
+ ultimo_time = datetime.utcfromtimestamp(int(ultimo[0]["time"]))
75
+ return ultimo_time.date()
76
+
77
+ # ======================================================
78
+ # RATES (CANDLE MODE)
79
+ # ======================================================
80
+
81
+ def obter_rates(self, timeframe, quantidade, ancorar_abertura=False):
82
+
83
+ with mt5_conexao():
84
+
85
+ if not mt5.symbol_select(self.symbol, True):
86
+ raise RuntimeError(f"Erro ao selecionar símbolo {self.symbol}")
87
+
88
+ if quantidade == 0:
89
+ quantidade = 1000
90
+
91
+ rates = mt5.copy_rates_from_pos(
92
+ self.symbol,
93
+ timeframe,
94
+ 0,
95
+ quantidade,
96
+ )
97
+
98
+ if rates is None or len(rates) == 0:
99
+ return []
100
+
101
+ if not ancorar_abertura:
102
+ return rates
103
+
104
+ # ----------------------------------------------------
105
+ # ANCORAGEM NA ÚLTIMA SESSÃO DISPONÍVEL
106
+ # ----------------------------------------------------
107
+
108
+ from datetime import timedelta
109
+
110
+ ultimo_ts = int(rates[-1]["time"])
111
+
112
+ ultimo_dt = datetime.fromtimestamp(ultimo_ts)
113
+
114
+ ultimo_dia = ultimo_dt.date()
115
+
116
+ abertura = datetime.combine(
117
+ ultimo_dia,
118
+ dtime.fromisoformat(SESSION_OPEN),
119
+ )
120
+
121
+ # ajuste B3 -> UTC
122
+ abertura = abertura - timedelta(hours=3)
123
+
124
+ abertura_ts = int(abertura.timestamp())
125
+
126
+ filtrados = []
127
+
128
+ for r in rates:
129
+
130
+ ts = int(r["time"])
131
+
132
+ if ts >= abertura_ts:
133
+ filtrados.append(r)
134
+
135
+ return filtrados
136
+
137
+ # ======================================================
138
+ # TICKS (BANCO + MT5)
139
+ # ======================================================
140
+
141
+ def obter_ticks(self, max_ticks=5000, ancorar_abertura=False):
142
+
143
+ from datetime import timedelta
144
+
145
+ last_time = self.repo._get_last_tick_time(self.symbol)
146
+
147
+ if last_time is None:
148
+
149
+ self.repo.sync(self.symbol, days_back=3)
150
+ last_time = self.repo._get_last_tick_time(self.symbol)
151
+
152
+ else:
153
+
154
+ self.repo.sync(self.symbol)
155
+
156
+ if last_time is None:
157
+ return []
158
+
159
+ end_ts = int(datetime.now().timestamp())
160
+
161
+ if ancorar_abertura:
162
+
163
+ data = datetime.fromtimestamp(last_time)
164
+
165
+ # 09:00 horário B3
166
+ abertura_b3 = datetime.combine(
167
+ data.date(),
168
+ datetime.strptime(SESSION_OPEN, "%H:%M").time(),
169
+ )
170
+
171
+ # converter B3 (UTC-3) → UTC
172
+ abertura_utc = abertura_b3 - timedelta(hours=3)
173
+
174
+ # margem de segurança
175
+ abertura_utc = abertura_utc + timedelta(seconds=50)
176
+
177
+ start_ts = int(abertura_utc.timestamp())
178
+
179
+ else:
180
+
181
+ start_ts = 0
182
+
183
+ rows = self.repo.get_ticks_between(
184
+ self.symbol,
185
+ start_ts,
186
+ end_ts,
187
+ )
188
+
189
+ if rows is None or len(rows) == 0:
190
+ return []
191
+
192
+ return rows[-max_ticks:]
193
+
194
+ # ======================================================
195
+ # RENKO CANDLE (PATH RECONSTRUCTION)
196
+ # ======================================================
197
+
198
+ def construir_renko(self, rates, modo="simples") -> List[Brick]:
199
+
200
+ if rates is None or len(rates) < 2:
201
+ return []
202
+
203
+ bricks: List[Brick] = []
204
+
205
+ last_price = float(rates[0]["open"])
206
+
207
+ for rate in rates[1:]:
208
+
209
+ open_p = float(rate["open"])
210
+ high = float(rate["high"])
211
+ low = float(rate["low"])
212
+ close = float(rate["close"])
213
+
214
+ # -------------------------------------------------
215
+ # PATH RECONSTRUCTION
216
+ # -------------------------------------------------
217
+
218
+ if close >= open_p:
219
+ path = [low, high, close]
220
+ else:
221
+ path = [high, low, close]
222
+
223
+ for price in path:
224
+
225
+ while price - last_price >= self.brick_size:
226
+
227
+ novo = last_price + self.brick_size
228
+
229
+ bricks.append(
230
+ Brick(
231
+ direction="up",
232
+ open=last_price,
233
+ close=novo,
234
+ )
235
+ )
236
+
237
+ last_price = novo
238
+
239
+ while last_price - price >= self.brick_size:
240
+
241
+ novo = last_price - self.brick_size
242
+
243
+ bricks.append(
244
+ Brick(
245
+ direction="down",
246
+ open=last_price,
247
+ close=novo,
248
+ )
249
+ )
250
+
251
+ last_price = novo
252
+
253
+ return bricks
254
+
255
+ # ======================================================
256
+ # RENKO TICK MODE
257
+ # ======================================================
258
+
259
+ def construir_renko_ticks(self, ticks) -> RenkoTickResult:
260
+
261
+ if ticks is None or len(ticks) < 2:
262
+ return RenkoTickResult([], None)
263
+
264
+ bricks: List[Brick] = []
265
+
266
+ last_price = float(ticks[0][3])
267
+
268
+ for tick in ticks[1:]:
269
+
270
+ price = float(tick[3])
271
+
272
+ while price - last_price >= self.brick_size:
273
+
274
+ novo = last_price + self.brick_size
275
+
276
+ bricks.append(
277
+ Brick("up", last_price, novo)
278
+ )
279
+
280
+ last_price = novo
281
+
282
+ while last_price - price >= self.brick_size:
283
+
284
+ novo = last_price - self.brick_size
285
+
286
+ bricks.append(
287
+ Brick("down", last_price, novo)
288
+ )
289
+
290
+ last_price = novo
291
+
292
+ # ------------------------------------------
293
+ # brick em formação
294
+ # ------------------------------------------
295
+
296
+ ultimo_preco = float(ticks[-1][3])
297
+
298
+ diferenca = ultimo_preco - last_price
299
+
300
+ em_formacao = None
301
+
302
+ if abs(diferenca) > 0:
303
+
304
+ direcao = "up" if diferenca > 0 else "down"
305
+
306
+ em_formacao = Brick(
307
+ direction=direcao,
308
+ open=last_price,
309
+ close=ultimo_preco,
310
+ )
311
+
312
+ return RenkoTickResult(
313
+ confirmados=bricks,
314
+ em_formacao=em_formacao,
315
+ )
@@ -0,0 +1,81 @@
1
+ """
2
+ Renko view acessível.
3
+ """
4
+
5
+ import click
6
+ from ..conf import DIGITS
7
+
8
+
9
+ def _detectar_padroes(bricks):
10
+
11
+ if len(bricks) < 3:
12
+ return []
13
+
14
+ patterns = []
15
+
16
+ last = bricks[-1].direction
17
+ prev = bricks[-2].direction
18
+ prev2 = bricks[-3].direction
19
+
20
+ if last == "up" and prev == "down":
21
+ patterns.append("H1")
22
+
23
+ if last == "up" and prev == "down" and prev2 == "up":
24
+ patterns.append("H2")
25
+
26
+ if last == "down" and prev == "up" and prev2 == "down":
27
+ patterns.append("L2")
28
+
29
+ return patterns
30
+
31
+
32
+ def _metricas(bricks):
33
+
34
+ up = sum(1 for b in bricks if b.direction == "up")
35
+ down = sum(1 for b in bricks if b.direction == "down")
36
+
37
+ return up, down
38
+
39
+
40
+ def exibir_renko(resultado, numerar=False):
41
+
42
+ if not resultado:
43
+ click.echo("Nenhum bloco Renko gerado.")
44
+ return
45
+
46
+ if isinstance(resultado, list):
47
+
48
+ bricks = resultado
49
+
50
+ else:
51
+
52
+ bricks = resultado.confirmados
53
+
54
+ click.echo("=== GRAFICO RENKO ===")
55
+ click.echo(f"Total de blocos: {len(bricks)}")
56
+ click.echo()
57
+
58
+ up, down = _metricas(bricks)
59
+
60
+ click.echo("METRICAS:")
61
+ click.echo(f"Up: {up}")
62
+ click.echo(f"Down: {down}")
63
+ click.echo(f"Delta: {up-down}")
64
+ click.echo()
65
+
66
+ patterns = _detectar_padroes(bricks)
67
+
68
+ if patterns:
69
+ click.echo("PADROES:")
70
+ for p in patterns:
71
+ click.echo(p)
72
+ click.echo()
73
+
74
+ for i, brick in enumerate(bricks, start=1):
75
+
76
+ if numerar:
77
+ linha = f"{i} {brick.direction.upper()} {brick.open:.{DIGITS}f} {brick.close:.{DIGITS}f}"
78
+ else:
79
+ linha = f"{brick.direction.upper()} {brick.open:.{DIGITS}f} {brick.close:.{DIGITS}f}"
80
+
81
+ click.echo(linha)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mtcli-renko"
3
- version = "1.1.0.dev2"
3
+ version = "1.1.0.dev3"
4
4
  description = "Renko plugin institucional para mtcli (MetaTrader 5)"
5
5
  authors = [
6
6
  { name = "Valmir França", email = "vfranca3@gmail.com" }
@@ -32,9 +32,9 @@ classifiers = [
32
32
  ]
33
33
 
34
34
  dependencies = [
35
- "mtcli>=3.2.0",
35
+ "mtcli>=3.5.0",
36
36
  "click>=8.3.0,<9.0.0",
37
- "metatrader5>=5.0.5370,<6.0.0"
37
+ "metatrader5>=5.0.5370,<6.0.0",
38
38
  ]
39
39
 
40
40
  [project.urls]
@@ -1,85 +0,0 @@
1
- """
2
- Comando CLI para geração de gráfico Renko.
3
- """
4
-
5
- import click
6
-
7
- from ..controllers.renko_controller import RenkoController
8
- from ..views.renko_view import exibir_renko
9
- from ..domain.timeframe import Timeframe
10
- from mtcli.logger import setup_logger
11
- from ..conf import SYMBOL, BRICK, PERIOD, BARS, DATA_MODE, MAX_TICKS, TICK_STYLE
12
-
13
- log = setup_logger(__name__)
14
-
15
-
16
- @click.command()
17
- @click.version_option(package_name="mtcli-renko")
18
- @click.option("--symbol", "-s", default=SYMBOL, show_default=True)
19
- @click.option("--brick", "-b", default=BRICK, show_default=True, type=float)
20
- @click.option("--timeframe", "-t", default=PERIOD, show_default=True)
21
- @click.option("--bars", "-n", default=BARS, show_default=True, type=int)
22
- @click.option("--numerar/--no-numerar", default=False, show_default=True)
23
- @click.option(
24
- "--modo",
25
- type=click.Choice(["simples", "classico"], case_sensitive=False),
26
- default="simples",
27
- show_default=True,
28
- )
29
- @click.option(
30
- "--ancorar-abertura",
31
- is_flag=True,
32
- default=False,
33
- show_default=True,
34
- )
35
- @click.option(
36
- "--data-mode",
37
- type=click.Choice(["candle", "tick"], case_sensitive=False),
38
- default=DATA_MODE,
39
- show_default=True,
40
- )
41
- @click.option(
42
- "--max-ticks",
43
- default=MAX_TICKS,
44
- show_default=True,
45
- type=int,
46
- )
47
- @click.option(
48
- "--tick-style",
49
- type=click.Choice(["estrutural", "hibrido", "agressivo"], case_sensitive=False),
50
- default=TICK_STYLE,
51
- show_default=True,
52
- help="Define como tratar brick em formação no modo tick.",
53
- )
54
- def renko(
55
- symbol,
56
- brick,
57
- timeframe,
58
- bars,
59
- numerar,
60
- modo,
61
- ancorar_abertura,
62
- data_mode,
63
- max_ticks,
64
- tick_style,
65
- ):
66
-
67
- try:
68
- tf_enum = Timeframe.from_string(timeframe)
69
- except ValueError as e:
70
- raise click.BadParameter(str(e))
71
-
72
- controller = RenkoController(
73
- symbol,
74
- brick,
75
- tf_enum.mt5_const,
76
- bars,
77
- modo,
78
- ancorar_abertura,
79
- data_mode,
80
- max_ticks,
81
- tick_style,
82
- )
83
-
84
- resultado = controller.executar()
85
- exibir_renko(resultado, numerar=numerar)
@@ -1,105 +0,0 @@
1
- """
2
- Renko controller.
3
-
4
- Responsável por:
5
- - Orquestrar obtenção de dados (candle ou tick)
6
- - Chamar o model
7
- - Aplicar estilo de saída no modo tick
8
- """
9
-
10
- from ..models.renko_model import RenkoModel
11
- from mtcli.logger import setup_logger
12
-
13
- log = setup_logger(__name__)
14
-
15
-
16
- class RenkoController:
17
- """
18
- Controller principal do Renko.
19
-
20
- :param symbol: ativo (ex: WINJ26)
21
- :param brick_size: tamanho do brick
22
- :param timeframe: timeframe MT5 (para candle)
23
- :param quantidade: número de candles
24
- :param modo: simples ou classico
25
- :param ancorar_abertura: ancora na abertura da sessão
26
- :param data_mode: candle ou tick
27
- :param max_ticks: quantidade máxima de ticks
28
- :param tick_style: estrutural | hibrido | agressivo
29
- """
30
-
31
- def __init__(
32
- self,
33
- symbol,
34
- brick_size,
35
- timeframe,
36
- quantidade,
37
- modo="simples",
38
- ancorar_abertura=False,
39
- data_mode="candle",
40
- max_ticks=3000,
41
- tick_style="hibrido",
42
- ):
43
- self.model = RenkoModel(symbol, brick_size)
44
- self.timeframe = timeframe
45
- self.quantidade = quantidade
46
- self.modo = modo
47
- self.ancorar_abertura = ancorar_abertura
48
- self.data_mode = data_mode
49
- self.max_ticks = max_ticks
50
- self.tick_style = tick_style
51
-
52
- def executar(self):
53
- """
54
- Executa construção do Renko conforme modo configurado.
55
- """
56
-
57
- # =========================
58
- # MODO TICK
59
- # =========================
60
- if self.data_mode == "tick":
61
-
62
- ticks = self.model.obter_ticks(
63
- timeframe=self.timeframe,
64
- max_ticks=self.max_ticks,
65
- )
66
-
67
- # 🔒 Correção definitiva do erro numpy
68
- if ticks is None or len(ticks) == 0:
69
- return []
70
-
71
- resultado = self.model.construir_renko_ticks(ticks)
72
-
73
- # =========================
74
- # Aplicar estilo de tick
75
- # =========================
76
-
77
- # Estrutural → somente confirmados
78
- if self.tick_style == "estrutural":
79
- return resultado.confirmados
80
-
81
- # Agressivo → confirmados + parcial como válido
82
- if self.tick_style == "agressivo":
83
- if resultado.em_formacao:
84
- return resultado.confirmados + [resultado.em_formacao]
85
- return resultado.confirmados
86
-
87
- # Híbrido (default) → retorna objeto completo
88
- return resultado
89
-
90
- # =========================
91
- # MODO CANDLE
92
- # =========================
93
- rates = self.model.obter_rates(
94
- self.timeframe,
95
- self.quantidade,
96
- ancorar_abertura=self.ancorar_abertura,
97
- )
98
-
99
- if rates is None or len(rates) == 0:
100
- return []
101
-
102
- return self.model.construir_renko(
103
- rates,
104
- modo=self.modo,
105
- )
@@ -1,87 +0,0 @@
1
- """
2
- Enum de Timeframes suportados pelo MTCLI Renko.
3
-
4
- Fornece:
5
- - Conversão amigável (m5, 5m, h1, 1h)
6
- - Mapeamento para constante MT5
7
- - Lista de valores válidos para CLI
8
- """
9
-
10
- from enum import Enum
11
- import MetaTrader5 as mt5
12
-
13
-
14
- class Timeframe(Enum):
15
- """
16
- Representa timeframes suportados pelo MT5.
17
- """
18
-
19
- M1 = ("m1", mt5.TIMEFRAME_M1)
20
- M2 = ("m2", mt5.TIMEFRAME_M2)
21
- M3 = ("m3", mt5.TIMEFRAME_M3)
22
- M4 = ("m4", mt5.TIMEFRAME_M4)
23
- M5 = ("m5", mt5.TIMEFRAME_M5)
24
- M10 = ("m10", mt5.TIMEFRAME_M10)
25
- M15 = ("m15", mt5.TIMEFRAME_M15)
26
- M30 = ("m30", mt5.TIMEFRAME_M30)
27
-
28
- H1 = ("h1", mt5.TIMEFRAME_H1)
29
- H2 = ("h2", mt5.TIMEFRAME_H2)
30
- H3 = ("h3", mt5.TIMEFRAME_H3)
31
- H4 = ("h4", mt5.TIMEFRAME_H4)
32
- H6 = ("h6", mt5.TIMEFRAME_H6)
33
- H8 = ("h8", mt5.TIMEFRAME_H8)
34
- H12 = ("h12", mt5.TIMEFRAME_H12)
35
-
36
- D1 = ("d1", mt5.TIMEFRAME_D1)
37
- W1 = ("w1", mt5.TIMEFRAME_W1)
38
- MN1 = ("mn1", mt5.TIMEFRAME_MN1)
39
-
40
- def __init__(self, label: str, mt5_const: int):
41
- self.label = label
42
- self.mt5_const = mt5_const
43
-
44
- @classmethod
45
- def from_string(cls, value: str) -> "Timeframe":
46
- """
47
- Converte string amigável para Enum Timeframe.
48
-
49
- Aceita:
50
- m5, 5m
51
- h1, 1h
52
- d1, 1d
53
- """
54
-
55
- value = value.strip().lower()
56
-
57
- # Aliases humanos
58
- aliases = {
59
- "1m": "m1",
60
- "5m": "m5",
61
- "15m": "m15",
62
- "30m": "m30",
63
- "1h": "h1",
64
- "4h": "h4",
65
- "1d": "d1",
66
- "1w": "w1",
67
- "1mo": "mn1",
68
- }
69
-
70
- if value in aliases:
71
- value = aliases[value]
72
-
73
- for tf in cls:
74
- if tf.label == value:
75
- return tf
76
-
77
- raise ValueError(
78
- f"Timeframe inválido: {value}. "
79
- f"Use: {', '.join(cls.valid_labels())}"
80
- )
81
-
82
- @classmethod
83
- def valid_labels(cls):
84
- """
85
- Retorna lista de labels válidos.
86
- """
87
- return [tf.label for tf in cls]
@@ -1,235 +0,0 @@
1
- """
2
- Renko model institucional profissional.
3
-
4
- ✔ Candle mode determinístico
5
- ✔ Tick mode híbrido (confirmados + em formação)
6
- ✔ Estrutura estável
7
- ✔ Compatível com controller atual
8
- """
9
-
10
- from dataclasses import dataclass
11
- from typing import List, Optional, NamedTuple
12
- from datetime import datetime
13
-
14
- import MetaTrader5 as mt5
15
-
16
- from mtcli.mt5_context import mt5_conexao
17
- from mtcli.logger import setup_logger
18
- from ..conf import SESSION_OPEN
19
-
20
- log = setup_logger(__name__)
21
-
22
-
23
- # ==========================================================
24
- # DATA STRUCTURES
25
- # ==========================================================
26
-
27
- @dataclass
28
- class RenkoBrick:
29
- direction: str
30
- open: float
31
- close: float
32
-
33
-
34
- class RenkoResult(NamedTuple):
35
- confirmados: List[RenkoBrick]
36
- em_formacao: Optional[RenkoBrick]
37
-
38
-
39
- # ==========================================================
40
- # MODEL
41
- # ==========================================================
42
-
43
- class RenkoModel:
44
-
45
- def __init__(self, symbol: str, brick_size: float):
46
- self.symbol = symbol
47
- self.brick_size = brick_size
48
-
49
- # ======================================================
50
- # AUXILIAR
51
- # ======================================================
52
-
53
- def _ultimo_pregao_data(self, timeframe):
54
-
55
- ultimo = mt5.copy_rates_from_pos(
56
- self.symbol,
57
- timeframe,
58
- 0,
59
- 1,
60
- )
61
-
62
- if ultimo is None or len(ultimo) == 0:
63
- return None
64
-
65
- ultimo_time = datetime.fromtimestamp(ultimo[0]["time"])
66
- return ultimo_time.date()
67
-
68
- # ======================================================
69
- # RATES (candle mode)
70
- # ======================================================
71
-
72
- def obter_rates(self, timeframe, quantidade: int, ancorar_abertura=False):
73
-
74
- with mt5_conexao():
75
-
76
- if not mt5.symbol_select(self.symbol, True):
77
- raise RuntimeError(f"Erro ao selecionar símbolo {self.symbol}")
78
-
79
- if not ancorar_abertura:
80
-
81
- if quantidade == 0:
82
- quantidade = 1000
83
-
84
- rates = mt5.copy_rates_from_pos(
85
- self.symbol,
86
- timeframe,
87
- 0,
88
- quantidade,
89
- )
90
-
91
- return rates or []
92
-
93
- data_pregao = self._ultimo_pregao_data(timeframe)
94
-
95
- if data_pregao is None:
96
- return []
97
-
98
- bruto = mt5.copy_rates_from_pos(
99
- self.symbol,
100
- timeframe,
101
- 0,
102
- 5000,
103
- )
104
-
105
- if bruto is None or len(bruto) == 0:
106
- return []
107
-
108
- filtrado = []
109
-
110
- for r in bruto:
111
- r_time = datetime.fromtimestamp(r["time"])
112
- if r_time.date() == data_pregao:
113
- filtrado.append(r)
114
-
115
- if quantidade == 0:
116
- return filtrado
117
-
118
- return filtrado[-quantidade:]
119
-
120
- # ======================================================
121
- # TICKS
122
- # ======================================================
123
-
124
- def obter_ticks(self, timeframe, max_ticks=5000):
125
-
126
- with mt5_conexao():
127
-
128
- if not mt5.symbol_select(self.symbol, True):
129
- raise RuntimeError(f"Erro ao selecionar símbolo {self.symbol}")
130
-
131
- data_pregao = self._ultimo_pregao_data(timeframe)
132
-
133
- if data_pregao is None:
134
- return []
135
-
136
- inicio = datetime.combine(
137
- data_pregao,
138
- datetime.strptime(SESSION_OPEN, "%H:%M").time(),
139
- )
140
-
141
- agora = datetime.now()
142
-
143
- ticks = mt5.copy_ticks_range(
144
- self.symbol,
145
- inicio,
146
- agora,
147
- mt5.COPY_TICKS_ALL,
148
- )
149
-
150
- if ticks is None or len(ticks) == 0:
151
- return []
152
-
153
- if len(ticks) > max_ticks:
154
- ticks = ticks[-max_ticks:]
155
-
156
- return ticks
157
-
158
- # ======================================================
159
- # CONSTRUÇÃO RENKO (CANDLE)
160
- # ======================================================
161
-
162
- def construir_renko(self, rates, modo="simples") -> List[RenkoBrick]:
163
-
164
- if rates is None or len(rates) < 2:
165
- return []
166
-
167
- bricks: List[RenkoBrick] = []
168
- last_price = float(rates[0]["open"])
169
-
170
- for rate in rates[1:]:
171
-
172
- high = float(rate["high"])
173
- low = float(rate["low"])
174
-
175
- while high - last_price >= self.brick_size:
176
- novo = last_price + self.brick_size
177
- bricks.append(RenkoBrick("up", last_price, novo))
178
- last_price = novo
179
-
180
- while last_price - low >= self.brick_size:
181
- novo = last_price - self.brick_size
182
- bricks.append(RenkoBrick("down", last_price, novo))
183
- last_price = novo
184
-
185
- return bricks
186
-
187
- # ======================================================
188
- # CONSTRUÇÃO RENKO (TICK HÍBRIDO)
189
- # ======================================================
190
-
191
- def construir_renko_ticks(self, ticks) -> RenkoResult:
192
-
193
- if ticks is None or len(ticks) < 2:
194
- return RenkoResult([], None)
195
-
196
- bricks: List[RenkoBrick] = []
197
- last_price = float(ticks[0]["last"])
198
-
199
- for tick in ticks[1:]:
200
-
201
- price = float(tick["last"])
202
-
203
- while price - last_price >= self.brick_size:
204
- novo = last_price + self.brick_size
205
- bricks.append(RenkoBrick("up", last_price, novo))
206
- last_price = novo
207
-
208
- while last_price - price >= self.brick_size:
209
- novo = last_price - self.brick_size
210
- bricks.append(RenkoBrick("down", last_price, novo))
211
- last_price = novo
212
-
213
- # ----------------------------
214
- # Brick em formação
215
- # ----------------------------
216
-
217
- ultimo_preco = float(ticks[-1]["last"])
218
- diferenca = ultimo_preco - last_price
219
-
220
- em_formacao = None
221
-
222
- if abs(diferenca) > 0:
223
-
224
- direcao = "up" if diferenca > 0 else "down"
225
-
226
- em_formacao = RenkoBrick(
227
- direction=direcao,
228
- open=last_price,
229
- close=ultimo_preco,
230
- )
231
-
232
- return RenkoResult(
233
- confirmados=bricks,
234
- em_formacao=em_formacao,
235
- )
File without changes
@@ -1,75 +0,0 @@
1
- """
2
- Renko view acessível.
3
- """
4
-
5
- import click
6
- from ..conf import DIGITS
7
-
8
-
9
- def exibir_renko(resultado, numerar: bool = False):
10
-
11
- if not resultado:
12
- click.echo("Nenhum bloco Renko gerado.")
13
- return
14
-
15
- # Lista simples (estrutural ou agressivo)
16
- if isinstance(resultado, list):
17
-
18
- click.echo("=== GRAFICO RENKO ===")
19
- click.echo(f"Total de blocos: {len(resultado)}")
20
- click.echo()
21
-
22
- for i, brick in enumerate(resultado, start=1):
23
-
24
- if numerar:
25
- linha = (
26
- f"{i} "
27
- f"{brick.direction.upper()} "
28
- f"{brick.open:.{DIGITS}f} "
29
- f"{brick.close:.{DIGITS}f}"
30
- )
31
- else:
32
- linha = (
33
- f"{brick.direction.upper()} "
34
- f"{brick.open:.{DIGITS}f} "
35
- f"{brick.close:.{DIGITS}f}"
36
- )
37
-
38
- click.echo(linha)
39
-
40
- return
41
-
42
- # Híbrido
43
- confirmados = resultado.confirmados
44
- em_formacao = resultado.em_formacao
45
-
46
- click.echo("=== GRAFICO RENKO ===")
47
- click.echo(f"Blocos confirmados: {len(confirmados)}")
48
- click.echo()
49
-
50
- for i, brick in enumerate(confirmados, start=1):
51
-
52
- if numerar:
53
- linha = (
54
- f"{i} "
55
- f"{brick.direction.upper()} "
56
- f"{brick.open:.{DIGITS}f} "
57
- f"{brick.close:.{DIGITS}f}"
58
- )
59
- else:
60
- linha = (
61
- f"{brick.direction.upper()} "
62
- f"{brick.open:.{DIGITS}f} "
63
- f"{brick.close:.{DIGITS}f}"
64
- )
65
-
66
- click.echo(linha)
67
-
68
- if em_formacao:
69
- click.echo()
70
- click.echo("EM FORMACAO:")
71
- click.echo(
72
- f"{em_formacao.direction.upper()} "
73
- f"{em_formacao.open:.{DIGITS}f} "
74
- f"{em_formacao.close:.{DIGITS}f}"
75
- )