mtcli-volume 2.0.0.dev3__tar.gz → 2.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mtcli-volume
3
- Version: 2.0.0.dev3
3
+ Version: 2.2.0.dev0
4
4
  Summary: Plugin mtcli para exibir o volume profile
5
5
  Author: Valmir França da Silva
6
6
  Author-email: vfranca3@gmail.com
@@ -10,6 +10,8 @@ Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Dist: click (>=8.3.0,<9.0.0)
14
+ Requires-Dist: metatrader5 (>=5.0.5370,<6.0.0)
13
15
  Requires-Dist: mtcli (>=3.2.0)
14
16
  Project-URL: Documentation, https://mtcli-volume.readthedocs.io
15
17
  Project-URL: Homepage, https://github.com/vfranca/mtcli-volume
@@ -1,10 +1,10 @@
1
- import os
2
-
3
- from mtcli.conf import config
4
-
5
- SYMBOL = os.getenv("SYMBOL", config["DEFAULT"].get("symbol", fallback="WIN$N"))
6
- DIGITOS = int(os.getenv("DIGITOS", config["DEFAULT"].getint("digitos", fallback=0)))
7
- PERIOD = os.getenv("PERIOD", config["DEFAULT"].get("period", fallback="M1"))
8
- BARS = int(os.getenv("BARS", config["DEFAULT"].getint("bars", fallback=566)))
9
- STEP = float(os.getenv("STEP", config["DEFAULT"].getfloat("step", fallback=100)))
10
- VOLUME = os.getenv("VOLUME", config["DEFAULT"].get("volume", fallback="tick"))
1
+ import os
2
+
3
+ from mtcli.conf import config
4
+
5
+ SYMBOL = os.getenv("SYMBOL", config["DEFAULT"].get("symbol", fallback="WIN$N"))
6
+ DIGITOS = int(os.getenv("DIGITOS", config["DEFAULT"].getint("digitos", fallback=0)))
7
+ PERIOD = os.getenv("PERIOD", config["DEFAULT"].get("period", fallback="M1"))
8
+ BARS = int(os.getenv("BARS", config["DEFAULT"].getint("bars", fallback=566)))
9
+ STEP = float(os.getenv("STEP", config["DEFAULT"].getfloat("step", fallback=100)))
10
+ VOLUME = os.getenv("VOLUME", config["DEFAULT"].get("volume", fallback="tick"))
@@ -1,26 +1,28 @@
1
- from mtcli.logger import setup_logger
2
- from mtcli_volume.models.volume_model import (
3
- calcular_estatisticas,
4
- calcular_profile,
5
- obter_rates,
6
- )
7
-
8
- log = setup_logger()
9
-
10
-
11
- def calcular_volume_profile(symbol, period, bars, step, volume):
12
- """Controla o fluxo de cálculo do volume profile."""
13
- volume = volume.lower().strip()
14
- if volume not in ["tick", "real"]:
15
- log.error(f"Tipo de volume inválido: {volume}. Use 'tick' ou 'real'.")
16
- raise ValueError(f"Tipo de volume inválido: {volume}. Use 'tick' ou 'real'.")
17
-
18
- rates = obter_rates(symbol, period, bars)
19
- if not rates:
20
- log.error("Falha ao obter dados de preços para cálculo do volume profile.")
21
- return {}, {}
22
-
23
- profile = calcular_profile(rates, step, volume)
24
- stats = calcular_estatisticas(profile)
25
-
26
- return profile, stats
1
+ from mtcli.logger import setup_logger
2
+ from mtcli_volume.models.volume_model import (
3
+ calcular_estatisticas,
4
+ calcular_profile,
5
+ obter_rates,
6
+ )
7
+
8
+ log = setup_logger()
9
+
10
+
11
+ def calcular_volume_profile(symbol, period, bars, step, volume, data_inicio=None, data_fim=None):
12
+ """Controla o fluxo de cálculo do volume profile."""
13
+ volume = volume.lower().strip()
14
+ if volume not in ["tick", "real"]:
15
+ log.error(f"Tipo de volume inválido: {volume}. Use 'tick' ou 'real'.")
16
+ raise ValueError(f"Tipo de volume inválido: {volume}. Use 'tick' ou 'real'.")
17
+
18
+ rates = obter_rates(symbol, period, bars, data_inicio, data_fim)
19
+
20
+ # Correção: checagem segura para arrays NumPy
21
+ if rates is None or len(rates) == 0:
22
+ log.error("Falha ao obter dados de preços para cálculo do volume profile.")
23
+ return {}, {}
24
+
25
+ profile = calcular_profile(rates, step, volume)
26
+ stats = calcular_estatisticas(profile)
27
+
28
+ return profile, stats
@@ -0,0 +1,126 @@
1
+ from collections import defaultdict
2
+ from collections.abc import Mapping
3
+ from typing import Any, Dict, List, Union
4
+ from datetime import datetime
5
+ import numpy as np
6
+
7
+ import MetaTrader5 as mt5
8
+
9
+ from mtcli.logger import setup_logger
10
+ from mtcli.mt5_context import mt5_conexao
11
+ from mtcli_volume.conf import DIGITOS
12
+
13
+ log = setup_logger()
14
+
15
+
16
+ def obter_rates(
17
+ symbol: str,
18
+ period: str,
19
+ bars: int,
20
+ data_inicio: datetime = None,
21
+ data_fim: datetime = None,
22
+ ):
23
+ """Obtém dados históricos via MetaTrader 5, podendo filtrar por intervalo de tempo."""
24
+ with mt5_conexao():
25
+ tf = getattr(mt5, f"TIMEFRAME_{period.upper()}", None)
26
+ if tf is None:
27
+ log.error(f"Timeframe inválido: {period}")
28
+ return None
29
+
30
+ if not mt5.symbol_select(symbol, True):
31
+ log.error(f"Erro ao selecionar símbolo {symbol}")
32
+ return None
33
+
34
+ try:
35
+ if data_inicio and data_fim:
36
+ log.debug(f"Obtendo candles de {symbol} entre {data_inicio} e {data_fim}")
37
+ rates = mt5.copy_rates_range(symbol, tf, data_inicio, data_fim)
38
+ else:
39
+ log.debug(f"Obtendo {bars} candles de {symbol} a partir da posição 0")
40
+ rates = mt5.copy_rates_from_pos(symbol, tf, 0, bars)
41
+ except Exception as e:
42
+ log.error(f"Erro ao obter dados históricos: {e}")
43
+ return None
44
+
45
+ # ✅ Correção para arrays NumPy
46
+ if rates is None or len(rates) == 0:
47
+ log.error("Nenhum dado retornado.")
48
+ return None
49
+
50
+ return rates
51
+
52
+
53
+ def calcular_profile(
54
+ rates: List[Union[dict, tuple, object]],
55
+ step: float,
56
+ volume: str = "tick",
57
+ ) -> Dict[float, float]:
58
+ """Calcula o volume total por faixa de preço, suportando dicionários, numpy.void, objetos ou tuplas."""
59
+ profile = defaultdict(int)
60
+
61
+ for r in rates:
62
+ # --- 1️⃣ Dict comum ---
63
+ if isinstance(r, Mapping):
64
+ preco = r["close"]
65
+ tick_volume = r["tick_volume"]
66
+ real_volume = r.get("real_volume", tick_volume)
67
+
68
+ # --- 2️⃣ numpy.void (estrutura MT5) ---
69
+ elif isinstance(r, np.void):
70
+ preco = float(r["close"])
71
+ tick_volume = int(r["tick_volume"])
72
+ # real_volume pode não existir dependendo da corretora
73
+ real_volume = int(r["real_volume"]) if "real_volume" in r.dtype.names else tick_volume
74
+
75
+ # --- 3️⃣ Objeto com atributos (ex: namedtuple) ---
76
+ elif hasattr(r, "close"):
77
+ preco = r.close
78
+ tick_volume = r.tick_volume
79
+ real_volume = getattr(r, "real_volume", tick_volume)
80
+
81
+ # --- 4️⃣ Tupla/lista ---
82
+ elif isinstance(r, (tuple, list)) and len(r) >= 6:
83
+ preco = r[4]
84
+ tick_volume = r[5]
85
+ real_volume = r[6] if len(r) > 6 else tick_volume
86
+
87
+ else:
88
+ raise TypeError(f"Formato de rate desconhecido: {type(r)}")
89
+
90
+ faixa = round(round(preco / step) * step, DIGITOS)
91
+ profile[faixa] += tick_volume if volume == "tick" else real_volume
92
+
93
+ return dict(profile)
94
+
95
+
96
+ def calcular_estatisticas(profile):
97
+ """Calcula POC, área de valor (70%), HVNs e LVNs."""
98
+ if not profile:
99
+ return {"poc": None, "area_valor": (None, None), "hvns": [], "lvns": []}
100
+
101
+ # Ordena as faixas de preço por volume (desc)
102
+ volumes_ordenados = sorted(profile.items(), key=lambda x: x[1], reverse=True)
103
+ poc = volumes_ordenados[0][0]
104
+
105
+ # Cálculo da área de valor (70%)
106
+ total_volume = sum(profile.values())
107
+ acumulado = 0
108
+ faixas_area_valor = []
109
+ for faixa, vol in volumes_ordenados:
110
+ acumulado += vol
111
+ faixas_area_valor.append(faixa)
112
+ if acumulado >= total_volume * 0.7:
113
+ break
114
+ area_valor = (min(faixas_area_valor), max(faixas_area_valor))
115
+
116
+ # HVNs e LVNs
117
+ media = total_volume / len(profile)
118
+ hvns = sorted([faixa for faixa, vol in profile.items() if vol >= media * 1.5])
119
+ lvns = sorted([faixa for faixa, vol in profile.items() if vol <= media * 0.5])
120
+
121
+ return {
122
+ "poc": poc,
123
+ "area_valor": area_valor,
124
+ "hvns": hvns,
125
+ "lvns": lvns,
126
+ }
@@ -1,4 +1,4 @@
1
- from mtcli_volume.commands.volume_cli import volume
1
+ from mtcli_volume.volume import volume
2
2
 
3
3
 
4
4
  def register(cli):
@@ -1,31 +1,29 @@
1
- import click
2
-
3
- from mtcli_volume.conf import DIGITOS
4
-
5
- BARRA_CHAR = "#" # Pode mudar para "■" ou "=" se UTF-8 for garantido
6
-
7
-
8
- def exibir_volume_profile(profile, stats, symbol):
9
- """Exibe o volume profile no terminal."""
10
- if not profile:
11
- click.echo(f"Nenhum dado disponível para {symbol}")
12
- return
13
-
14
- dados_ordenados = sorted(profile.items(), reverse=True)
15
-
16
- click.echo(f"\nVolume Profile {symbol}\n")
17
- max_vol = max(profile.values())
18
- for preco, vol in dados_ordenados:
19
- barra = BARRA_CHAR * (vol // max(1, max_vol // 50))
20
- click.echo(f"{preco:>8.{DIGITOS}f} {vol:>6} {barra}")
21
-
22
- # Estatísticas
23
- if stats.get("poc") is not None:
24
- click.echo(f"\nPOC (Preço de Maior Volume): {stats['poc']:.{DIGITOS}f}")
25
- click.echo(
26
- f"Área de Valor: {stats['area_valor'][0]:.{DIGITOS}f} a {stats['area_valor'][1]:.{DIGITOS}f}"
27
- )
28
- click.echo(f"HVNs: {stats['hvns']}")
29
- click.echo(f"LVNs: {stats['lvns']}")
30
- else:
31
- click.echo("\nEstatísticas indisponíveis (dados insuficientes).")
1
+ import click
2
+
3
+ from mtcli_volume.conf import DIGITOS
4
+
5
+ BARRA_CHAR = "#" # Pode mudar para "■" ou "=" se UTF-8 for garantido
6
+
7
+
8
+ def exibir_volume_profile(profile, stats, symbol):
9
+ """Exibe o volume profile no terminal."""
10
+ if not profile:
11
+ click.echo(f"Nenhum dado disponível para {symbol}")
12
+ return
13
+
14
+ dados_ordenados = sorted(profile.items(), reverse=True)
15
+
16
+ click.echo(f"\nVolume Profile {symbol}\n")
17
+ max_vol = max(profile.values())
18
+ for preco, vol in dados_ordenados:
19
+ barra = BARRA_CHAR * (vol // max(1, max_vol // 50))
20
+ click.echo(f"{preco:>8.{DIGITOS}f} {vol:>6} {barra}")
21
+
22
+ # Estatísticas
23
+ if stats.get("poc") is not None:
24
+ click.echo(f"\nPOC {stats['poc']:.{DIGITOS}f}")
25
+ click.echo(f"VA {stats['area_valor'][0]:.{DIGITOS}f} a {stats['area_valor'][1]:.{DIGITOS}f}")
26
+ click.echo(f"HVNs {stats['hvns']}")
27
+ click.echo(f"LVNs {stats['lvns']}")
28
+ else:
29
+ click.echo("\nEstatísticas indisponíveis (dados insuficientes).")
@@ -0,0 +1,31 @@
1
+ import click
2
+ from datetime import datetime
3
+
4
+ from mtcli_volume.conf import BARS, PERIOD, STEP, SYMBOL, VOLUME
5
+ from mtcli_volume.controllers.volume_controller import calcular_volume_profile
6
+ from mtcli_volume.views.volume_view import exibir_volume_profile
7
+
8
+
9
+ @click.command(
10
+ "volume",
11
+ help="Exibe o Volume Profile, agrupando volumes por faixa de preço no histórico recente.",
12
+ )
13
+ @click.version_option(package_name="mtcli-volume")
14
+ @click.option("--symbol", "-s", default=SYMBOL, show_default=True, help="Símbolo do ativo.")
15
+ @click.option("--period", "-p", default=PERIOD, show_default=True, help="Timeframe (ex: M1, M5, H1).")
16
+ @click.option("--bars", "-b", default=BARS, show_default=True, help="Número de barras.")
17
+ @click.option("--step", "-e", type=float, default=STEP, show_default=True, help="Tamanho do agrupamento de preços.")
18
+ @click.option("--volume", "-v", default=VOLUME, show_default=True, help="Tipo de volume (tick ou real).")
19
+ @click.option("--from", "data_inicio", type=str, help="Data/hora inicial no formato 'YYYY-MM-DD HH:MM'.")
20
+ @click.option("--to", "data_fim", type=str, help="Data/hora final no formato 'YYYY-MM-DD HH:MM'.")
21
+ def volume(symbol, period, bars, step, volume, data_inicio, data_fim):
22
+ """Exibe o Volume Profile agrupando volumes por faixa de preço."""
23
+ inicio = datetime.strptime(data_inicio, "%Y-%m-%d %H:%M") if data_inicio else None
24
+ fim = datetime.strptime(data_fim, "%Y-%m-%d %H:%M") if data_fim else None
25
+
26
+ profile, stats = calcular_volume_profile(symbol, period, bars, step, volume, inicio, fim)
27
+ exibir_volume_profile(profile, stats, symbol)
28
+
29
+
30
+ if __name__ == "__main__":
31
+ volume()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mtcli-volume"
3
- version = "2.0.0.dev3"
3
+ version = "2.2.0.dev0"
4
4
  description = "Plugin mtcli para exibir o volume profile"
5
5
  authors = [
6
6
  {name = "Valmir França da Silva",email = "vfranca3@gmail.com"}
@@ -8,7 +8,9 @@ authors = [
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.10,<3.14.0"
10
10
  dependencies = [
11
- "mtcli>=3.2.0"
11
+ "mtcli>=3.2.0",
12
+ "click (>=8.3.0,<9.0.0)",
13
+ "metatrader5 (>=5.0.5370,<6.0.0)"
12
14
  ]
13
15
 
14
16
  [project.urls]
@@ -1,39 +0,0 @@
1
- import click
2
-
3
- from mtcli_volume.conf import BARS, PERIOD, STEP, SYMBOL, VOLUME
4
- from mtcli_volume.controllers.volume_controller import calcular_volume_profile
5
- from mtcli_volume.views.volume_view import exibir_volume_profile
6
-
7
-
8
- @click.command(
9
- "volume",
10
- help="Exibe o Volume Profile, agrupando volumes por faixa de preço no histórico recente.",
11
- )
12
- @click.version_option(package_name="mtcli-volume")
13
- @click.option(
14
- "--symbol", "-s", default=SYMBOL, show_default=True, help="Simbolo do ativo."
15
- )
16
- @click.option("--period", "-p", default=PERIOD, show_default=True, help="Timeframe (ex: M1, M5, H1).")
17
- @click.option("--bars", "-b", default=BARS, show_default=True, help="Numero de barras.")
18
- @click.option(
19
- "--step",
20
- "-e",
21
- type=float,
22
- default=STEP, show_default=True,
23
- help="Tamanho do agrupamento de precos.",
24
- )
25
- @click.option(
26
- "--volume",
27
- "-v",
28
- default=VOLUME,
29
- show_default=True,
30
- help="Tipo de volume (tick ou real).",
31
- )
32
- def volume(symbol, period, bars, step, volume):
33
- """Exibe o Volume Profile agrupando volumes por faixa de preço."""
34
- profile, stats = calcular_volume_profile(symbol, period, bars, step, volume)
35
- exibir_volume_profile(profile, stats, symbol)
36
-
37
-
38
- if __name__ == "__main__":
39
- volume()
@@ -1,98 +0,0 @@
1
- from collections import defaultdict
2
-
3
- import MetaTrader5 as mt5
4
-
5
- from mtcli.logger import setup_logger
6
- from mtcli.models.rates_model import RatesModel
7
- from mtcli.mt5_context import mt5_conexao
8
- from mtcli_volume.conf import DIGITOS
9
-
10
- log = setup_logger()
11
-
12
-
13
- def obter_rates(symbol, period, bars):
14
- """Obtém os dados históricos de preços via MetaTrader 5."""
15
- with mt5_conexao():
16
- tf = getattr(mt5, f"TIMEFRAME_{period.upper()}", None)
17
- if tf is None:
18
- log.error(f"Timeframe inválido: {period}")
19
- return
20
-
21
- if not mt5.symbol_select(symbol, True):
22
- log.error(f"Erro ao selecionar símbolo {symbol}")
23
- return
24
-
25
- rates = RatesModel(symbol, period, bars).get_data()
26
-
27
- if not rates:
28
- log.error("Erro: não foi possível obter os dados históricos.")
29
- return
30
-
31
- return rates
32
-
33
-
34
- def calcular_profile(rates, step, volume="tick"):
35
- """Calcula o volume total por faixa de preço, suportando dicionários, objetos ou tuplas."""
36
- from collections.abc import Mapping
37
-
38
- profile = defaultdict(int)
39
-
40
- for r in rates:
41
- # Detecta se r é dict, objeto com atributos ou tupla
42
- if isinstance(r, Mapping): # dict
43
- preco = r["close"]
44
- tick_volume = r["tick_volume"]
45
- real_volume = r.get("real_volume", tick_volume)
46
- elif hasattr(r, "close"): # objeto com atributos
47
- preco = r.close
48
- tick_volume = r.tick_volume
49
- real_volume = getattr(r, "real_volume", tick_volume)
50
- elif isinstance(r, (tuple, list)) and len(r) >= 6: # tupla com campos esperados
51
- preco = r[4] # índice típico de 'close' em rates do MT5
52
- tick_volume = r[5]
53
- real_volume = r[6] if len(r) > 6 else tick_volume
54
- else:
55
- raise TypeError(f"Formato de rate desconhecido: {type(r)}")
56
-
57
- faixa = round(round(preco / step) * step, DIGITOS)
58
- profile[faixa] += tick_volume if volume == "tick" else real_volume
59
-
60
- return dict(profile)
61
-
62
-
63
- def calcular_estatisticas(profile):
64
- """Calcula POC, área de valor (70%), HVNs e LVNs."""
65
- if profile is None or len(profile) == 0:
66
- return {
67
- "poc": None,
68
- "area_valor": (None, None),
69
- "hvns": [],
70
- "lvns": [],
71
- }
72
-
73
- # Ordena faixas de preço pelo volume em ordem decrescente
74
- volumes_ordenados = sorted(profile.items(), key=lambda x: x[1], reverse=True)
75
- poc = volumes_ordenados[0][0]
76
-
77
- # Área de valor (70% do volume)
78
- total_volume = sum(profile.values())
79
- acumulado = 0
80
- faixas_area_valor = []
81
- for faixa, vol in volumes_ordenados:
82
- acumulado += vol
83
- faixas_area_valor.append(faixa)
84
- if acumulado >= total_volume * 0.7:
85
- break
86
- area_valor = (min(faixas_area_valor), max(faixas_area_valor))
87
-
88
- # HVNs (High Volume Nodes) e LVNs (Low Volume Nodes)
89
- media = total_volume / len(profile)
90
- hvns = sorted([faixa for faixa, vol in profile.items() if vol >= media * 1.5])
91
- lvns = sorted([faixa for faixa, vol in profile.items() if vol <= media * 0.5])
92
-
93
- return {
94
- "poc": poc,
95
- "area_valor": area_valor,
96
- "hvns": hvns,
97
- "lvns": lvns,
98
- }