mtcli-volume 2.2.0.dev0__tar.gz → 2.2.0.dev1__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_volume-2.2.0.dev0 → mtcli_volume-2.2.0.dev1}/PKG-INFO +1 -1
- mtcli_volume-2.2.0.dev1/mtcli_volume/controllers/volume_controller.py +83 -0
- {mtcli_volume-2.2.0.dev0 → mtcli_volume-2.2.0.dev1}/mtcli_volume/models/volume_model.py +9 -6
- mtcli_volume-2.2.0.dev1/mtcli_volume/views/volume_view.py +69 -0
- mtcli_volume-2.2.0.dev1/mtcli_volume/volume.py +69 -0
- {mtcli_volume-2.2.0.dev0 → mtcli_volume-2.2.0.dev1}/pyproject.toml +1 -1
- mtcli_volume-2.2.0.dev0/mtcli_volume/controllers/volume_controller.py +0 -28
- mtcli_volume-2.2.0.dev0/mtcli_volume/views/volume_view.py +0 -29
- mtcli_volume-2.2.0.dev0/mtcli_volume/volume.py +0 -31
- {mtcli_volume-2.2.0.dev0 → mtcli_volume-2.2.0.dev1}/LICENSE +0 -0
- {mtcli_volume-2.2.0.dev0 → mtcli_volume-2.2.0.dev1}/README.md +0 -0
- {mtcli_volume-2.2.0.dev0 → mtcli_volume-2.2.0.dev1}/mtcli_volume/__init__.py +0 -0
- {mtcli_volume-2.2.0.dev0 → mtcli_volume-2.2.0.dev1}/mtcli_volume/conf.py +0 -0
- {mtcli_volume-2.2.0.dev0 → mtcli_volume-2.2.0.dev1}/mtcli_volume/plugin.py +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
|
2
|
+
import zoneinfo # Python 3.9+
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from mtcli.logger import setup_logger
|
|
7
|
+
from mtcli_volume.models.volume_model import (
|
|
8
|
+
calcular_estatisticas,
|
|
9
|
+
calcular_profile,
|
|
10
|
+
obter_rates,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
log = setup_logger()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def calcular_volume_profile(
|
|
17
|
+
symbol,
|
|
18
|
+
period,
|
|
19
|
+
bars,
|
|
20
|
+
step,
|
|
21
|
+
volume,
|
|
22
|
+
data_inicio=None,
|
|
23
|
+
data_fim=None,
|
|
24
|
+
verbose=False,
|
|
25
|
+
timezone_str="America/Sao_Paulo",
|
|
26
|
+
):
|
|
27
|
+
"""Controla o fluxo de cálculo do volume profile, com suporte a timezone configurável."""
|
|
28
|
+
volume = volume.lower().strip()
|
|
29
|
+
if volume not in ["tick", "real"]:
|
|
30
|
+
log.error(f"Tipo de volume inválido: {volume}. Use 'tick' ou 'real'.")
|
|
31
|
+
raise ValueError(f"Tipo de volume inválido: {volume}. Use 'tick' ou 'real'.")
|
|
32
|
+
|
|
33
|
+
rates = obter_rates(symbol, period, bars, data_inicio, data_fim)
|
|
34
|
+
|
|
35
|
+
if rates is None or len(rates) == 0:
|
|
36
|
+
log.error("Falha ao obter dados de preços para cálculo do volume profile.")
|
|
37
|
+
return {}, {}, {}
|
|
38
|
+
|
|
39
|
+
profile = calcular_profile(rates, step, volume)
|
|
40
|
+
stats = calcular_estatisticas(profile)
|
|
41
|
+
|
|
42
|
+
# Captura de informações de contexto
|
|
43
|
+
info = {}
|
|
44
|
+
if isinstance(rates, np.ndarray) and len(rates) > 0:
|
|
45
|
+
primeiro = rates[0]
|
|
46
|
+
ultimo = rates[-1]
|
|
47
|
+
if "time" in rates.dtype.names:
|
|
48
|
+
try:
|
|
49
|
+
# Define fuso horário desejado
|
|
50
|
+
try:
|
|
51
|
+
fuso = zoneinfo.ZoneInfo(timezone_str)
|
|
52
|
+
except Exception:
|
|
53
|
+
log.warning(
|
|
54
|
+
f"Fuso horário '{timezone_str}' inválido. Usando UTC−3 (Brasília)."
|
|
55
|
+
)
|
|
56
|
+
fuso = timezone(timedelta(hours=-3))
|
|
57
|
+
|
|
58
|
+
inicio_real = (
|
|
59
|
+
datetime.utcfromtimestamp(float(primeiro["time"]))
|
|
60
|
+
.astimezone(fuso)
|
|
61
|
+
.strftime("%Y-%m-%d %H:%M:%S")
|
|
62
|
+
)
|
|
63
|
+
fim_real = (
|
|
64
|
+
datetime.utcfromtimestamp(float(ultimo["time"]))
|
|
65
|
+
.astimezone(fuso)
|
|
66
|
+
.strftime("%Y-%m-%d %H:%M:%S")
|
|
67
|
+
)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
log.error(f"Erro ao converter timezone: {e}")
|
|
70
|
+
inicio_real = fim_real = "?"
|
|
71
|
+
else:
|
|
72
|
+
inicio_real = fim_real = "?"
|
|
73
|
+
|
|
74
|
+
info = {
|
|
75
|
+
"symbol": symbol,
|
|
76
|
+
"period": period,
|
|
77
|
+
"candles": len(rates),
|
|
78
|
+
"inicio": inicio_real,
|
|
79
|
+
"fim": fim_real,
|
|
80
|
+
"timezone": timezone_str,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return profile, stats, info
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
2
|
from collections.abc import Mapping
|
|
3
|
-
from typing import Any, Dict, List, Union
|
|
4
3
|
from datetime import datetime
|
|
5
|
-
import numpy as np
|
|
6
4
|
|
|
7
5
|
import MetaTrader5 as mt5
|
|
6
|
+
import numpy as np
|
|
8
7
|
|
|
9
8
|
from mtcli.logger import setup_logger
|
|
10
9
|
from mtcli.mt5_context import mt5_conexao
|
|
@@ -33,7 +32,9 @@ def obter_rates(
|
|
|
33
32
|
|
|
34
33
|
try:
|
|
35
34
|
if data_inicio and data_fim:
|
|
36
|
-
log.debug(
|
|
35
|
+
log.debug(
|
|
36
|
+
f"Obtendo candles de {symbol} entre {data_inicio} e {data_fim}"
|
|
37
|
+
)
|
|
37
38
|
rates = mt5.copy_rates_range(symbol, tf, data_inicio, data_fim)
|
|
38
39
|
else:
|
|
39
40
|
log.debug(f"Obtendo {bars} candles de {symbol} a partir da posição 0")
|
|
@@ -51,10 +52,10 @@ def obter_rates(
|
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
def calcular_profile(
|
|
54
|
-
rates:
|
|
55
|
+
rates: list[dict | tuple | object],
|
|
55
56
|
step: float,
|
|
56
57
|
volume: str = "tick",
|
|
57
|
-
) ->
|
|
58
|
+
) -> dict[float, float]:
|
|
58
59
|
"""Calcula o volume total por faixa de preço, suportando dicionários, numpy.void, objetos ou tuplas."""
|
|
59
60
|
profile = defaultdict(int)
|
|
60
61
|
|
|
@@ -70,7 +71,9 @@ def calcular_profile(
|
|
|
70
71
|
preco = float(r["close"])
|
|
71
72
|
tick_volume = int(r["tick_volume"])
|
|
72
73
|
# real_volume pode não existir dependendo da corretora
|
|
73
|
-
real_volume =
|
|
74
|
+
real_volume = (
|
|
75
|
+
int(r["real_volume"]) if "real_volume" in r.dtype.names else tick_volume
|
|
76
|
+
)
|
|
74
77
|
|
|
75
78
|
# --- 3️⃣ Objeto com atributos (ex: namedtuple) ---
|
|
76
79
|
elif hasattr(r, "close"):
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from mtcli_volume.conf import DIGITOS
|
|
4
|
+
|
|
5
|
+
BARRA_CHAR = "#"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def exibir_volume_profile(profile, stats, symbol, info=None, verbose=False):
|
|
9
|
+
"""Exibe o volume profile no terminal de forma acessível e organizada."""
|
|
10
|
+
if not profile:
|
|
11
|
+
click.echo(f"Nenhum dado disponível para {symbol}")
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
# ──────────────────────────────────────────────
|
|
15
|
+
# BLOCO VERBOSO: exibe detalhes da análise
|
|
16
|
+
# ──────────────────────────────────────────────
|
|
17
|
+
if verbose and info:
|
|
18
|
+
click.echo("\n=== Informações da Análise ===")
|
|
19
|
+
linhas = [
|
|
20
|
+
("Símbolo", info.get("symbol", "?")),
|
|
21
|
+
("Timeframe", info.get("period", "?").upper()),
|
|
22
|
+
("Candles analisados", str(info.get("candles", "?"))),
|
|
23
|
+
(
|
|
24
|
+
"Período analisado",
|
|
25
|
+
f"{info.get('inicio', '?')} → {info.get('fim', '?')}",
|
|
26
|
+
),
|
|
27
|
+
("Fuso horário", info.get("timezone", "Desconhecido")),
|
|
28
|
+
]
|
|
29
|
+
largura_esq = max(len(t[0]) for t in linhas) + 2
|
|
30
|
+
for chave, valor in linhas:
|
|
31
|
+
click.echo(f"{chave:<{largura_esq}}: {valor}")
|
|
32
|
+
click.echo("=" * (largura_esq + 30))
|
|
33
|
+
|
|
34
|
+
# ──────────────────────────────────────────────
|
|
35
|
+
# BLOCO PRINCIPAL: Volume Profile
|
|
36
|
+
# ──────────────────────────────────────────────
|
|
37
|
+
dados_ordenados = sorted(profile.items(), reverse=True)
|
|
38
|
+
click.echo(f"\n📊 Volume Profile — {symbol}\n")
|
|
39
|
+
|
|
40
|
+
max_vol = max(profile.values())
|
|
41
|
+
largura_preco = max(len(f"{p:.{DIGITOS}f}") for p in profile.keys())
|
|
42
|
+
|
|
43
|
+
# Cabeçalho
|
|
44
|
+
click.echo(f"{'Preço':>{largura_preco}} | Volume | Distribuição")
|
|
45
|
+
click.echo("-" * (largura_preco + 32))
|
|
46
|
+
|
|
47
|
+
# Corpo da tabela
|
|
48
|
+
for preco, vol in dados_ordenados:
|
|
49
|
+
barra_len = int(vol / max_vol * 50)
|
|
50
|
+
barra = BARRA_CHAR * barra_len
|
|
51
|
+
click.echo(f"{preco:>{largura_preco}.{DIGITOS}f} | {vol:>6} | {barra}")
|
|
52
|
+
|
|
53
|
+
# ──────────────────────────────────────────────
|
|
54
|
+
# BLOCO FINAL: Estatísticas
|
|
55
|
+
# ──────────────────────────────────────────────
|
|
56
|
+
click.echo("\n=== Estatísticas ===")
|
|
57
|
+
if stats.get("poc") is not None:
|
|
58
|
+
click.echo(f"POC : {stats['poc']:.{DIGITOS}f}")
|
|
59
|
+
click.echo(
|
|
60
|
+
f"Área de Valor : {stats['area_valor'][0]:.{DIGITOS}f} → {stats['area_valor'][1]:.{DIGITOS}f}"
|
|
61
|
+
)
|
|
62
|
+
click.echo(
|
|
63
|
+
f"HVNs (High Vol.) : {', '.join(map(lambda x: f'{x:.{DIGITOS}f}', stats['hvns'])) or 'Nenhum'}"
|
|
64
|
+
)
|
|
65
|
+
click.echo(
|
|
66
|
+
f"LVNs (Low Vol.) : {', '.join(map(lambda x: f'{x:.{DIGITOS}f}', stats['lvns'])) or 'Nenhum'}"
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
click.echo("Estatísticas indisponíveis (dados insuficientes).")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from mtcli_volume.conf import BARS, PERIOD, STEP, SYMBOL, VOLUME
|
|
6
|
+
from mtcli_volume.controllers.volume_controller import calcular_volume_profile
|
|
7
|
+
from mtcli_volume.views.volume_view import exibir_volume_profile
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command(
|
|
11
|
+
"volume",
|
|
12
|
+
help="Exibe o Volume Profile, agrupando volumes por faixa de preço no histórico recente.",
|
|
13
|
+
)
|
|
14
|
+
@click.version_option(package_name="mtcli-volume")
|
|
15
|
+
@click.option(
|
|
16
|
+
"--symbol", "-s", default=SYMBOL, show_default=True, help="Símbolo do ativo."
|
|
17
|
+
)
|
|
18
|
+
@click.option(
|
|
19
|
+
"--period",
|
|
20
|
+
"-p",
|
|
21
|
+
default=PERIOD,
|
|
22
|
+
show_default=True,
|
|
23
|
+
help="Timeframe (ex: M1, M5, H1).",
|
|
24
|
+
)
|
|
25
|
+
@click.option("--bars", "-b", default=BARS, show_default=True, help="Número de barras.")
|
|
26
|
+
@click.option(
|
|
27
|
+
"--step",
|
|
28
|
+
"-e",
|
|
29
|
+
type=float,
|
|
30
|
+
default=STEP,
|
|
31
|
+
show_default=True,
|
|
32
|
+
help="Tamanho do agrupamento de preços.",
|
|
33
|
+
)
|
|
34
|
+
@click.option(
|
|
35
|
+
"--volume",
|
|
36
|
+
"-v",
|
|
37
|
+
default=VOLUME,
|
|
38
|
+
show_default=True,
|
|
39
|
+
help="Tipo de volume (tick ou real).",
|
|
40
|
+
)
|
|
41
|
+
@click.option(
|
|
42
|
+
"--from", "data_inicio", type=str, help="Data/hora inicial (YYYY-MM-DD HH:MM)."
|
|
43
|
+
)
|
|
44
|
+
@click.option("--to", "data_fim", type=str, help="Data/hora final (YYYY-MM-DD HH:MM).")
|
|
45
|
+
@click.option(
|
|
46
|
+
"--verbose",
|
|
47
|
+
"-vv",
|
|
48
|
+
is_flag=True,
|
|
49
|
+
help="Mostra informações detalhadas sobre a análise.",
|
|
50
|
+
)
|
|
51
|
+
@click.option(
|
|
52
|
+
"--timezone",
|
|
53
|
+
"-tz",
|
|
54
|
+
type=str,
|
|
55
|
+
default="America/Sao_Paulo",
|
|
56
|
+
show_default=True,
|
|
57
|
+
help="Fuso horário para exibição das datas (ex: 'UTC', 'America/Sao_Paulo').",
|
|
58
|
+
)
|
|
59
|
+
def volume(
|
|
60
|
+
symbol, period, bars, step, volume, data_inicio, data_fim, verbose, timezone
|
|
61
|
+
):
|
|
62
|
+
"""Exibe o Volume Profile agrupando volumes por faixa de preço."""
|
|
63
|
+
inicio = datetime.strptime(data_inicio, "%Y-%m-%d %H:%M") if data_inicio else None
|
|
64
|
+
fim = datetime.strptime(data_fim, "%Y-%m-%d %H:%M") if data_fim else None
|
|
65
|
+
|
|
66
|
+
profile, stats, info = calcular_volume_profile(
|
|
67
|
+
symbol, period, bars, step, volume, inicio, fim, verbose, timezone
|
|
68
|
+
)
|
|
69
|
+
exibir_volume_profile(profile, stats, symbol, info, verbose)
|
|
@@ -1,28 +0,0 @@
|
|
|
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
|
|
@@ -1,29 +0,0 @@
|
|
|
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).")
|
|
@@ -1,31 +0,0 @@
|
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|