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.
- {mtcli_renko-1.1.0.dev2 → mtcli_renko-1.1.0.dev3}/PKG-INFO +2 -2
- mtcli_renko-1.1.0.dev3/mtcli_renko/commands/renko.py +105 -0
- {mtcli_renko-1.1.0.dev2 → mtcli_renko-1.1.0.dev3}/mtcli_renko/conf.py +9 -0
- mtcli_renko-1.1.0.dev3/mtcli_renko/controllers/renko_controller.py +154 -0
- mtcli_renko-1.1.0.dev3/mtcli_renko/models/renko_model.py +315 -0
- mtcli_renko-1.1.0.dev3/mtcli_renko/views/renko_view.py +81 -0
- {mtcli_renko-1.1.0.dev2 → mtcli_renko-1.1.0.dev3}/pyproject.toml +3 -3
- mtcli_renko-1.1.0.dev2/mtcli_renko/commands/renko.py +0 -85
- mtcli_renko-1.1.0.dev2/mtcli_renko/controllers/renko_controller.py +0 -105
- mtcli_renko-1.1.0.dev2/mtcli_renko/domain/timeframe.py +0 -87
- mtcli_renko-1.1.0.dev2/mtcli_renko/models/renko_model.py +0 -235
- mtcli_renko-1.1.0.dev2/mtcli_renko/views/__init__.py +0 -0
- mtcli_renko-1.1.0.dev2/mtcli_renko/views/renko_view.py +0 -75
- {mtcli_renko-1.1.0.dev2 → mtcli_renko-1.1.0.dev3}/LICENSE +0 -0
- {mtcli_renko-1.1.0.dev2 → mtcli_renko-1.1.0.dev3}/README.md +0 -0
- {mtcli_renko-1.1.0.dev2 → mtcli_renko-1.1.0.dev3}/mtcli_renko/__init__.py +0 -0
- {mtcli_renko-1.1.0.dev2 → mtcli_renko-1.1.0.dev3}/mtcli_renko/commands/__init__.py +0 -0
- {mtcli_renko-1.1.0.dev2 → mtcli_renko-1.1.0.dev3}/mtcli_renko/controllers/__init__.py +0 -0
- {mtcli_renko-1.1.0.dev2/mtcli_renko/domain → mtcli_renko-1.1.0.dev3/mtcli_renko/models}/__init__.py +0 -0
- {mtcli_renko-1.1.0.dev2 → mtcli_renko-1.1.0.dev3}/mtcli_renko/plugin.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mtcli_renko-1.1.0.dev2/mtcli_renko/domain → mtcli_renko-1.1.0.dev3/mtcli_renko/models}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{mtcli_renko-1.1.0.dev2/mtcli_renko/models → mtcli_renko-1.1.0.dev3/mtcli_renko/views}/__init__.py
RENAMED
|
File without changes
|