mtcli-range 1.0.0.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Valmir França
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: mtcli-range
3
+ Version: 1.0.0.dev0
4
+ Summary: Range bars plugin para mtcli
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Keywords: trading,range,metatrader5,mt5,cli,price-action,tape-reading
8
+ Author: Valmir França
9
+ Author-email: vfranca3@gmail.com
10
+ Requires-Python: >=3.10,<3.14
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Financial and Insurance Industry
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Topic :: Office/Business :: Financial :: Investment
20
+ Requires-Dist: click (>=8.3.0,<9.0.0)
21
+ Requires-Dist: metatrader5 (>=5.0.5370,<6.0.0)
22
+ Requires-Dist: mtcli (==3.5.*)
23
+ Project-URL: Documentation, https://vfranca.github.io/mtcli-range
24
+ Project-URL: Homepage, https://github.com/vfranca/mtcli-range
25
+ Project-URL: Issues, https://github.com/vfranca/mtcli-range/issues
26
+ Project-URL: Repository, https://github.com/vfranca/mtcli-range
27
+ Description-Content-Type: text/markdown
28
+
29
+ \# plugin-exemplo
30
+
31
+ &nbsp;
32
+
33
+ Plugin do mtcli que adiciona o comando plugin de exemplo.
34
+
35
+ &nbsp;
36
+
37
+ ---
38
+
39
+ &nbsp;
40
+
41
+ \## Instalação
42
+
43
+ &nbsp;
44
+
45
+ ```cmd
46
+
47
+ git clone git@github.com:vfranca/plugin-exemplo.git
48
+
49
+ cd plugin-exemplo
50
+
51
+ pip install .
52
+
53
+ ```
54
+
55
+
56
+
57
+
@@ -0,0 +1,28 @@
1
+ \# plugin-exemplo
2
+
3
+ &nbsp;
4
+
5
+ Plugin do mtcli que adiciona o comando plugin de exemplo.
6
+
7
+ &nbsp;
8
+
9
+ ---
10
+
11
+ &nbsp;
12
+
13
+ \## Instalação
14
+
15
+ &nbsp;
16
+
17
+ ```cmd
18
+
19
+ git clone git@github.com:vfranca/plugin-exemplo.git
20
+
21
+ cd plugin-exemplo
22
+
23
+ pip install .
24
+
25
+ ```
26
+
27
+
28
+
File without changes
@@ -0,0 +1,54 @@
1
+ """
2
+ Interface CLI do plugin mtcli-range.
3
+
4
+ Comando:
5
+ mt range
6
+ """
7
+
8
+ import click
9
+ from mtcli.logger import setup_logger
10
+ from .range_controller import RangeController
11
+ from .conf import (
12
+ get_default_symbol,
13
+ get_default_range,
14
+ get_default_timeframe,
15
+ get_default_bars,
16
+ )
17
+
18
+ log = setup_logger("mtcli.range.cli")
19
+
20
+
21
+ @click.command("range")
22
+ @click.version_option(package_name="mtcli-range")
23
+ @click.option("-s", "--symbol", default=get_default_symbol(), show_default=True, help="Ativo.")
24
+ @click.option("-t", "--timeframe", default=get_default_timeframe(), show_default=True, help="Timeframe base.")
25
+ @click.option("-b", "--bars", default=get_default_bars(), show_default=True, help="Quantidade de candles base.")
26
+ @click.option("-r", "--range-size", default=get_default_range(), show_default=True, help="Tamanho do range.")
27
+ @click.option("--ancorar-abertura", is_flag=True, help="Inicia o Range a partir da abertura do primeiro candle.")
28
+ @click.option("--numerar", is_flag=True, help="Numera as linhas exibidas.")
29
+ def range_command(symbol, timeframe, bars, range_size, ancorar_abertura, numerar):
30
+ """
31
+ Exibe gráfico de Range no terminal.
32
+
33
+ Exemplos:
34
+
35
+ mt range -s WING26 -t m5 -b 1000 -r 60
36
+ mt range --ancorar-abertura
37
+ mt range --numerar
38
+ """
39
+
40
+ try:
41
+ controller = RangeController(
42
+ symbol=symbol,
43
+ timeframe=timeframe,
44
+ bars=bars,
45
+ range_size=range_size,
46
+ ancorar_abertura=ancorar_abertura,
47
+ numerar=numerar,
48
+ )
49
+
50
+ controller.executar()
51
+
52
+ except Exception as e:
53
+ log.error(f"Erro ao executar mt range: {e}")
54
+ click.echo(f"Erro: {e}")
@@ -0,0 +1,41 @@
1
+ """
2
+ Configuração do plugin mtcli-range.
3
+
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
10
+ """
11
+
12
+ import configparser
13
+ import os
14
+
15
+
16
+ CONFIG_PATH = os.path.join(os.getcwd(), "mtcli.ini")
17
+
18
+ config = configparser.ConfigParser()
19
+ config.read(CONFIG_PATH)
20
+
21
+ SECTION = "range"
22
+
23
+
24
+ def get_default_symbol() -> str:
25
+ """Retorna símbolo padrão do arquivo .ini."""
26
+ return config.get(SECTION, "symbol", fallback="WIN$")
27
+
28
+
29
+ def get_default_range() -> float:
30
+ """Retorna tamanho do range padrão."""
31
+ return config.getfloat(SECTION, "range_size", fallback=50.0)
32
+
33
+
34
+ def get_default_timeframe() -> str:
35
+ """Retorna timeframe base padrão."""
36
+ return config.get(SECTION, "timeframe", fallback="m5")
37
+
38
+
39
+ def get_default_bars() -> int:
40
+ """Retorna quantidade padrão de candles base."""
41
+ return config.getint(SECTION, "bars", fallback=500)
@@ -0,0 +1,6 @@
1
+ from .cli import range_command
2
+
3
+
4
+ def register(cli):
5
+ cli.add_command(range_command, name="range")
6
+ cli.add_command(range_command, name="r")
@@ -0,0 +1,65 @@
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
+ )
@@ -0,0 +1,103 @@
1
+ """
2
+ Model responsável pela geração dos blocos Range.
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).
7
+ """
8
+
9
+ from dataclasses import dataclass
10
+ from typing import List
11
+ import MetaTrader5 as mt5
12
+ from mtcli.logger import setup_logger
13
+
14
+ log = setup_logger("mtcli.range.model")
15
+
16
+
17
+ @dataclass
18
+ class RangeBlock:
19
+ """Representa um bloco de Range."""
20
+ open: float
21
+ close: float
22
+ direction: str # "UP" ou "DOWN"
23
+
24
+
25
+ class RangeModel:
26
+ """
27
+ Responsável por:
28
+ - Buscar dados no MT5
29
+ - Calcular blocos de Range
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ symbol: str,
35
+ timeframe_const: int,
36
+ bars: int,
37
+ range_size: float,
38
+ ancorar_abertura: bool = False,
39
+ ):
40
+ self.symbol = symbol
41
+ self.timeframe_const = timeframe_const
42
+ self.bars = bars
43
+ self.range_size = range_size
44
+ self.ancorar_abertura = ancorar_abertura
45
+
46
+ def obter_candles(self):
47
+ """Obtém candles do MT5."""
48
+ log.info(f"Buscando {self.bars} candles de {self.symbol}")
49
+ rates = mt5.copy_rates_from_pos(
50
+ self.symbol,
51
+ self.timeframe_const,
52
+ 0,
53
+ self.bars
54
+ )
55
+
56
+ if rates is None:
57
+ raise RuntimeError("Erro ao obter dados do MT5.")
58
+
59
+ return rates
60
+
61
+ def gerar_range(self) -> List[RangeBlock]:
62
+ """
63
+ Constrói blocos de Range a partir dos candles base.
64
+ """
65
+
66
+ rates = self.obter_candles()
67
+
68
+ if len(rates) == 0:
69
+ return []
70
+
71
+ blocos: List[RangeBlock] = []
72
+
73
+ # 🔹 Âncora inicial
74
+ if self.ancorar_abertura:
75
+ ultimo_preco_base = rates[0]["open"]
76
+ log.info("Range ancorado na abertura do primeiro candle.")
77
+ else:
78
+ ultimo_preco_base = rates[0]["close"]
79
+ log.info("Range iniciado a partir do fechamento do primeiro candle.")
80
+
81
+ for candle in rates[1:]:
82
+ preco = candle["close"]
83
+
84
+ while abs(preco - ultimo_preco_base) >= self.range_size:
85
+
86
+ if preco > ultimo_preco_base:
87
+ novo_close = ultimo_preco_base + self.range_size
88
+ direcao = "UP"
89
+ else:
90
+ novo_close = ultimo_preco_base - self.range_size
91
+ direcao = "DOWN"
92
+
93
+ bloco = RangeBlock(
94
+ open=ultimo_preco_base,
95
+ close=novo_close,
96
+ direction=direcao
97
+ )
98
+
99
+ blocos.append(bloco)
100
+ ultimo_preco_base = novo_close
101
+
102
+ log.info(f"{len(blocos)} blocos de range gerados.")
103
+ return blocos
@@ -0,0 +1,66 @@
1
+ """
2
+ View responsável pela saída no terminal.
3
+
4
+ Layout linear, amigável para leitores de tela.
5
+ Sem uso de gráficos ASCII complexos.
6
+ """
7
+
8
+ from typing import List
9
+ from mtcli.logger import setup_logger
10
+ from .range_model import RangeBlock
11
+
12
+ log = setup_logger("mtcli.range.view")
13
+
14
+
15
+ class RangeView:
16
+ """
17
+ Responsável por exibir os blocos de forma textual estruturada.
18
+ """
19
+
20
+ @staticmethod
21
+ def exibir_header(symbol: str, range_size: float, ancorado: bool):
22
+ """Exibe cabeçalho descritivo."""
23
+ print("\n========================================")
24
+ print("GRÁFICO RANGE")
25
+ print("----------------------------------------")
26
+ print(f"Ativo: {symbol}")
27
+ print(f"Tamanho do Range: {range_size}")
28
+ print(f"Ancorado na abertura: {'SIM' if ancorado else 'NÃO'}")
29
+ print("========================================\n")
30
+
31
+ @staticmethod
32
+ def exibir_blocos(blocos: List[RangeBlock], numerar: bool):
33
+ """
34
+ Exibe blocos linha a linha.
35
+
36
+ numerar:
37
+ - True -> exibe número da linha
38
+ - False -> não exibe numeração
39
+ """
40
+
41
+ if not blocos:
42
+ print("Nenhum bloco gerado.")
43
+ return
44
+
45
+ if numerar:
46
+ print("Número Direção Abertura Fechamento")
47
+ print("-----------------------------------")
48
+ else:
49
+ print("Direção Abertura Fechamento")
50
+ print("----------------------------")
51
+
52
+ for i, bloco in enumerate(blocos, start=1):
53
+
54
+ if numerar:
55
+ print(
56
+ f"{i:>6} "
57
+ f"{bloco.direction:<5} "
58
+ f"{bloco.open:>8.0f} "
59
+ f"{bloco.close:>10.0f}"
60
+ )
61
+ else:
62
+ print(
63
+ f"{bloco.direction:<5} "
64
+ f"{bloco.open:>8.0f} "
65
+ f"{bloco.close:>10.0f}"
66
+ )
@@ -0,0 +1,81 @@
1
+ [project]
2
+ name = "mtcli-range"
3
+ version = "1.0.0.dev0"
4
+ description = "Range bars plugin para mtcli"
5
+ authors = [
6
+ { name = "Valmir França", email = "vfranca3@gmail.com" }
7
+ ]
8
+ readme = "README.md"
9
+ license = "MIT"
10
+ license-files = ["LICENSE"]
11
+ requires-python = ">=3.10,<3.14"
12
+
13
+ keywords = [
14
+ "trading",
15
+ "range",
16
+ "metatrader5",
17
+ "mt5",
18
+ "cli",
19
+ "price-action",
20
+ "tape-reading"
21
+ ]
22
+
23
+ classifiers = [
24
+ "Development Status :: 4 - Beta",
25
+ "Intended Audience :: Financial and Insurance Industry",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "Operating System :: OS Independent",
32
+ "Topic :: Office/Business :: Financial :: Investment",
33
+ ]
34
+
35
+ dependencies = [
36
+ "mtcli>=3.5.0.dev0,<3.6.0.dev0",
37
+ "click>=8.3.0,<9.0.0",
38
+ "metatrader5>=5.0.5370,<6.0.0"
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/vfranca/mtcli-range"
43
+ Repository = "https://github.com/vfranca/mtcli-range"
44
+ Issues = "https://github.com/vfranca/mtcli-range/issues"
45
+ Documentation = "https://vfranca.github.io/mtcli-range"
46
+
47
+ [project.entry-points."mtcli.plugins"]
48
+ range = "mtcli_range.plugin:register"
49
+
50
+ [build-system]
51
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
52
+ build-backend = "poetry.core.masonry.api"
53
+
54
+ [tool.poetry.group.dev.dependencies]
55
+ pytest = "^8.4.2"
56
+ pytest-cov = "^6.2.1"
57
+ pytest-mock = "^3.15.1"
58
+ ruff = "^0.14.1"
59
+
60
+ [tool.poetry.group.docs.dependencies]
61
+ mkdocs = "^1.6.1"
62
+ pymdown-extensions = "^10.16.1"
63
+
64
+ [tool.ruff]
65
+ line-length = 88
66
+ target-version = "py311"
67
+
68
+ [tool.ruff.lint]
69
+ select = ["E", "F", "B", "I", "UP", "N"]
70
+ ignore = ["E501"]
71
+
72
+ [tool.ruff.lint.isort]
73
+ known-first-party = ["mtcli", "mtcli_range"]
74
+ combine-as-imports = true
75
+ force-sort-within-sections = true
76
+
77
+ [tool.ruff.format]
78
+ quote-style = "double"
79
+ indent-style = "space"
80
+ line-ending = "auto"
81
+ skip-magic-trailing-comma = false