mtcli-risco 2.4.1__tar.gz → 2.5.0__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_risco-2.5.0/LICENSE +21 -0
- {mtcli_risco-2.4.1 → mtcli_risco-2.5.0}/PKG-INFO +9 -4
- {mtcli_risco-2.4.1 → mtcli_risco-2.5.0}/mtcli_risco/cli.py +33 -27
- {mtcli_risco-2.4.1 → mtcli_risco-2.5.0}/mtcli_risco/commands/monitorar.py +64 -64
- {mtcli_risco-2.4.1 → mtcli_risco-2.5.0}/mtcli_risco/commands/panic.py +2 -2
- mtcli_risco-2.5.0/mtcli_risco/commands/start.py +78 -0
- mtcli_risco-2.5.0/mtcli_risco/commands/status.py +57 -0
- mtcli_risco-2.5.0/mtcli_risco/commands/stop.py +29 -0
- {mtcli_risco-2.4.1 → mtcli_risco-2.5.0}/mtcli_risco/conf.py +1 -1
- {mtcli_risco-2.4.1 → mtcli_risco-2.5.0}/mtcli_risco/models/checar_model.py +117 -117
- {mtcli_risco-2.4.1 → mtcli_risco-2.5.0}/mtcli_risco/models/hardstop_model.py +1 -1
- {mtcli_risco-2.4.1 → mtcli_risco-2.5.0}/mtcli_risco/models/panic_model.py +217 -217
- {mtcli_risco-2.4.1 → mtcli_risco-2.5.0}/mtcli_risco/models/trades_model.py +62 -62
- mtcli_risco-2.5.0/mtcli_risco/services/__init__.py +0 -0
- mtcli_risco-2.5.0/mtcli_risco/services/monitor_service.py +106 -0
- mtcli_risco-2.5.0/mtcli_risco/services/run_monitor.py +59 -0
- {mtcli_risco-2.4.1 → mtcli_risco-2.5.0}/pyproject.toml +30 -5
- mtcli_risco-2.4.1/LICENSE +0 -674
- {mtcli_risco-2.4.1 → mtcli_risco-2.5.0}/README.md +0 -0
- {mtcli_risco-2.4.1 → mtcli_risco-2.5.0}/mtcli_risco/__init__.py +0 -0
- {mtcli_risco-2.4.1 → mtcli_risco-2.5.0}/mtcli_risco/commands/__init__.py +0 -0
- {mtcli_risco-2.4.1 → mtcli_risco-2.5.0}/mtcli_risco/commands/checar.py +0 -0
- {mtcli_risco-2.4.1 → mtcli_risco-2.5.0}/mtcli_risco/commands/trades.py +0 -0
- {mtcli_risco-2.4.1 → mtcli_risco-2.5.0}/mtcli_risco/models/__init__.py +0 -0
- {mtcli_risco-2.4.1 → mtcli_risco-2.5.0}/mtcli_risco/plugin.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 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.
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mtcli-risco
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
4
4
|
Summary: Plugin mtcli para controle de loss diário
|
|
5
|
-
License-Expression:
|
|
5
|
+
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
7
|
-
|
|
7
|
+
Keywords: trading,risk manager,risk,metatrader5,mt5,cli,acessibilidade,screen reader friendly
|
|
8
|
+
Author: Valmir França
|
|
8
9
|
Author-email: vfranca3@gmail.com
|
|
9
10
|
Requires-Python: >=3.10,<3.14
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
10
13
|
Classifier: Programming Language :: Python :: 3
|
|
11
14
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
15
20
|
Requires-Dist: click (>=8.3.1,<9.0.0)
|
|
16
21
|
Requires-Dist: metatrader5 (>=5.0.5572,<6.0.0)
|
|
17
|
-
Requires-Dist: mtcli (>=
|
|
22
|
+
Requires-Dist: mtcli (>=3.7.2)
|
|
18
23
|
Project-URL: Documentation, https://mtcli-risco.readthedocs.io
|
|
19
24
|
Project-URL: Homepage, https://github.com/vfranca/mtcli-risco
|
|
20
25
|
Project-URL: Repository, https://github.com/vfranca/mtcli-risco
|
|
@@ -1,27 +1,33 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Comando principal e registro dos subcomandos
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import click
|
|
6
|
-
from .commands.checar import checar_cmd
|
|
7
|
-
from .commands.monitorar import monitorar_cmd
|
|
8
|
-
from .commands.trades import trades_cmd
|
|
9
|
-
from .commands.panic import panic_cmd
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
cli.add_command(
|
|
1
|
+
"""
|
|
2
|
+
Comando principal e registro dos subcomandos
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from .commands.checar import checar_cmd
|
|
7
|
+
from .commands.monitorar import monitorar_cmd
|
|
8
|
+
from .commands.trades import trades_cmd
|
|
9
|
+
from .commands.panic import panic_cmd
|
|
10
|
+
from .commands.start import start_cmd
|
|
11
|
+
from .commands.stop import stop_cmd
|
|
12
|
+
from .commands.status import status_cmd
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group()
|
|
16
|
+
@click.version_option(package_name="mtcli-risco")
|
|
17
|
+
def cli():
|
|
18
|
+
"""
|
|
19
|
+
Plugin mtcli-risco.
|
|
20
|
+
|
|
21
|
+
Conjunto de comandos para gerenciamento e controle de risco diário
|
|
22
|
+
baseado em lucro/prejuízo no MetaTrader 5.
|
|
23
|
+
"""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
cli.add_command(checar_cmd, name="checar")
|
|
28
|
+
cli.add_command(monitorar_cmd, name="monitorar")
|
|
29
|
+
cli.add_command(trades_cmd, name="trades")
|
|
30
|
+
cli.add_command(panic_cmd, name="panic")
|
|
31
|
+
cli.add_command(start_cmd, name="start")
|
|
32
|
+
cli.add_command(stop_cmd, name="stop")
|
|
33
|
+
cli.add_command(status_cmd, name="status")
|
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Comando monitorar.
|
|
3
|
-
|
|
4
|
-
Monitora continuamente o risco diário e executa PANIC CLOSE
|
|
5
|
-
quando o limite é excedido.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import time
|
|
9
|
-
import click
|
|
10
|
-
from datetime import date
|
|
11
|
-
from mtcli.logger import setup_logger
|
|
12
|
-
from
|
|
13
|
-
from
|
|
14
|
-
carregar_estado,
|
|
15
|
-
salvar_estado,
|
|
16
|
-
risco_excedido,
|
|
17
|
-
)
|
|
18
|
-
from
|
|
19
|
-
|
|
20
|
-
log = setup_logger()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@click.command()
|
|
24
|
-
@click.version_option(package_name="mtcli-risco")
|
|
25
|
-
@click.option("--limite", "-l", default=LOSS_LIMIT, show_default=True)
|
|
26
|
-
@click.option("--intervalo", "-i", default=INTERVALO, show_default=True)
|
|
27
|
-
@click.option("--dry-run", is_flag=True, help="Simula o panic close.")
|
|
28
|
-
def monitorar_cmd(limite: float, intervalo: int, dry_run: bool):
|
|
29
|
-
"""
|
|
30
|
-
Inicia o monitoramento contínuo do risco diário.
|
|
31
|
-
"""
|
|
32
|
-
click.echo(
|
|
33
|
-
f"Monitorando risco
|
|
34
|
-
)
|
|
35
|
-
log.info(
|
|
36
|
-
f"[MONITOR] Iniciado | limite={limite} intervalo={intervalo}s dry_run={dry_run}"
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
try:
|
|
40
|
-
while True:
|
|
41
|
-
hoje = date.today()
|
|
42
|
-
estado = carregar_estado(STATUS_FILE)
|
|
43
|
-
|
|
44
|
-
if estado["data"] != hoje.isoformat():
|
|
45
|
-
salvar_estado(STATUS_FILE, hoje, False)
|
|
46
|
-
estado["bloqueado"] = False
|
|
47
|
-
|
|
48
|
-
if not estado["bloqueado"] and risco_excedido(limite):
|
|
49
|
-
click.echo("LIMITE DE RISCO EXCEDIDO — PANIC CLOSE")
|
|
50
|
-
log.critical("[MONITOR] Disparando PANIC CLOSE")
|
|
51
|
-
|
|
52
|
-
resultado = panic_close_all(
|
|
53
|
-
retry_on_market_open=True,
|
|
54
|
-
dry_run=dry_run,
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
salvar_estado(STATUS_FILE, hoje, True)
|
|
58
|
-
log.critical(f"[MONITOR] Panic finalizado: {resultado}")
|
|
59
|
-
|
|
60
|
-
time.sleep(intervalo)
|
|
61
|
-
|
|
62
|
-
except KeyboardInterrupt:
|
|
63
|
-
click.echo("Monitoramento interrompido.")
|
|
64
|
-
log.info("[MONITOR] Interrompido pelo usuário.")
|
|
1
|
+
"""
|
|
2
|
+
Comando monitorar.
|
|
3
|
+
|
|
4
|
+
Monitora continuamente o risco diário e executa PANIC CLOSE
|
|
5
|
+
quando o limite é excedido.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
import click
|
|
10
|
+
from datetime import date
|
|
11
|
+
from mtcli.logger import setup_logger
|
|
12
|
+
from ..conf import LOSS_LIMIT, STATUS_FILE, INTERVALO
|
|
13
|
+
from ..models.checar_model import (
|
|
14
|
+
carregar_estado,
|
|
15
|
+
salvar_estado,
|
|
16
|
+
risco_excedido,
|
|
17
|
+
)
|
|
18
|
+
from ..models.panic_model import panic_close_all
|
|
19
|
+
|
|
20
|
+
log = setup_logger("risco")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.command()
|
|
24
|
+
@click.version_option(package_name="mtcli-risco")
|
|
25
|
+
@click.option("--limite", "-l", default=LOSS_LIMIT, show_default=True)
|
|
26
|
+
@click.option("--intervalo", "-i", default=INTERVALO, show_default=True)
|
|
27
|
+
@click.option("--dry-run", is_flag=True, help="Simula o panic close.")
|
|
28
|
+
def monitorar_cmd(limite: float, intervalo: int, dry_run: bool):
|
|
29
|
+
"""
|
|
30
|
+
Inicia o monitoramento contínuo do risco diário.
|
|
31
|
+
"""
|
|
32
|
+
click.echo(
|
|
33
|
+
f"Monitorando risco limite={limite:.2f} intervalo={intervalo}s"
|
|
34
|
+
)
|
|
35
|
+
log.info(
|
|
36
|
+
f"[MONITOR] Iniciado | limite={limite} intervalo={intervalo}s dry_run={dry_run}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
while True:
|
|
41
|
+
hoje = date.today()
|
|
42
|
+
estado = carregar_estado(STATUS_FILE)
|
|
43
|
+
|
|
44
|
+
if estado["data"] != hoje.isoformat():
|
|
45
|
+
salvar_estado(STATUS_FILE, hoje, False)
|
|
46
|
+
estado["bloqueado"] = False
|
|
47
|
+
|
|
48
|
+
if not estado["bloqueado"] and risco_excedido(limite):
|
|
49
|
+
click.echo("LIMITE DE RISCO EXCEDIDO — PANIC CLOSE")
|
|
50
|
+
log.critical("[MONITOR] Disparando PANIC CLOSE")
|
|
51
|
+
|
|
52
|
+
resultado = panic_close_all(
|
|
53
|
+
retry_on_market_open=True,
|
|
54
|
+
dry_run=dry_run,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
salvar_estado(STATUS_FILE, hoje, True)
|
|
58
|
+
log.critical(f"[MONITOR] Panic finalizado: {resultado}")
|
|
59
|
+
|
|
60
|
+
time.sleep(intervalo)
|
|
61
|
+
|
|
62
|
+
except KeyboardInterrupt:
|
|
63
|
+
click.echo("Monitoramento interrompido.")
|
|
64
|
+
log.info("[MONITOR] Interrompido pelo usuário.")
|
|
@@ -4,10 +4,10 @@ Comando PANIC CLOSE.
|
|
|
4
4
|
|
|
5
5
|
import click
|
|
6
6
|
from mtcli.logger import setup_logger
|
|
7
|
-
from
|
|
7
|
+
from ..models.panic_model import panic_close_all
|
|
8
8
|
from mtcli_risco import conf
|
|
9
9
|
|
|
10
|
-
log = setup_logger()
|
|
10
|
+
log = setup_logger("risco")
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
@click.command()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comando start.
|
|
3
|
+
|
|
4
|
+
Responsável por iniciar o monitor de risco em background.
|
|
5
|
+
|
|
6
|
+
Este comando cria um novo processo Python que executa o módulo
|
|
7
|
+
`mtcli_risco.services.run_monitor`, responsável por iniciar o
|
|
8
|
+
loop de monitoramento contínuo do risco diário.
|
|
9
|
+
|
|
10
|
+
Fluxo
|
|
11
|
+
-----
|
|
12
|
+
|
|
13
|
+
mt risco start
|
|
14
|
+
->
|
|
15
|
+
subprocess.Popen()
|
|
16
|
+
->
|
|
17
|
+
run_monitor.py
|
|
18
|
+
->
|
|
19
|
+
monitor_loop()
|
|
20
|
+
|
|
21
|
+
O comando também verifica se já existe um processo ativo
|
|
22
|
+
através do arquivo PID.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
import click
|
|
27
|
+
import subprocess
|
|
28
|
+
import sys
|
|
29
|
+
|
|
30
|
+
from mtcli.logger import setup_logger
|
|
31
|
+
from mtcli.conf import PID_FILE
|
|
32
|
+
from ..conf import LOSS_LIMIT, INTERVALO
|
|
33
|
+
|
|
34
|
+
log = setup_logger("risco")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@click.command()
|
|
38
|
+
@click.option("--limite", default=LOSS_LIMIT, show_default=True)
|
|
39
|
+
@click.option("--intervalo", default=INTERVALO, show_default=True)
|
|
40
|
+
@click.option("--dry-run", is_flag=True, help="Simula execução sem enviar ordens.")
|
|
41
|
+
def start_cmd(limite: float, intervalo: int, dry_run: bool) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Inicia o monitor de risco em background.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
limite : float
|
|
48
|
+
Limite máximo de prejuízo diário permitido.
|
|
49
|
+
intervalo : int
|
|
50
|
+
Intervalo (segundos) entre verificações de risco.
|
|
51
|
+
dry_run : bool
|
|
52
|
+
Executa em modo simulação (sem enviar ordens ao MT5).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
if os.path.exists(PID_FILE):
|
|
56
|
+
click.echo("Monitor já está rodando.")
|
|
57
|
+
log.warning("[START] Monitor já ativo | pid_file=%s", PID_FILE)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
cmd = [
|
|
61
|
+
sys.executable,
|
|
62
|
+
"-m",
|
|
63
|
+
"mtcli_risco.services.run_monitor",
|
|
64
|
+
str(limite),
|
|
65
|
+
str(intervalo),
|
|
66
|
+
str(int(dry_run)),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
log.info(
|
|
70
|
+
"[START] Iniciando monitor | limite=%.2f intervalo=%ss dry_run=%s",
|
|
71
|
+
limite,
|
|
72
|
+
intervalo,
|
|
73
|
+
dry_run,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
subprocess.Popen(cmd)
|
|
77
|
+
|
|
78
|
+
click.echo("Monitor iniciado em background.")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comando status.
|
|
3
|
+
|
|
4
|
+
Exibe o estado atual do monitor de risco.
|
|
5
|
+
|
|
6
|
+
O comando verifica:
|
|
7
|
+
|
|
8
|
+
1. Existência do arquivo PID
|
|
9
|
+
2. Presença do heartbeat
|
|
10
|
+
3. Tempo desde o último heartbeat
|
|
11
|
+
|
|
12
|
+
Isso permite detectar se o monitor:
|
|
13
|
+
|
|
14
|
+
- está rodando
|
|
15
|
+
- travou
|
|
16
|
+
- ou foi encerrado.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import time
|
|
21
|
+
import click
|
|
22
|
+
|
|
23
|
+
from mtcli.logger import setup_logger
|
|
24
|
+
from mtcli.conf import PID_FILE, HEARTBEAT_FILE
|
|
25
|
+
|
|
26
|
+
log = setup_logger("risco")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@click.command()
|
|
30
|
+
def status_cmd() -> None:
|
|
31
|
+
"""
|
|
32
|
+
Exibe o status atual do monitor de risco.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
if not os.path.exists(PID_FILE):
|
|
36
|
+
click.echo("Monitor não está rodando.")
|
|
37
|
+
log.info("[STATUS] Monitor inativo")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
with open(PID_FILE) as f:
|
|
41
|
+
pid = f.read().strip()
|
|
42
|
+
|
|
43
|
+
if os.path.exists(HEARTBEAT_FILE):
|
|
44
|
+
|
|
45
|
+
with open(HEARTBEAT_FILE) as f:
|
|
46
|
+
ts = float(f.read())
|
|
47
|
+
|
|
48
|
+
age = int(time.time() - ts)
|
|
49
|
+
|
|
50
|
+
click.echo(f"Monitor ativo (PID {pid})")
|
|
51
|
+
click.echo(f"Último heartbeat: {age}s")
|
|
52
|
+
|
|
53
|
+
log.debug("[STATUS] Monitor ativo | pid=%s heartbeat_age=%ss", pid, age)
|
|
54
|
+
|
|
55
|
+
else:
|
|
56
|
+
click.echo(f"Monitor rodando (PID {pid})")
|
|
57
|
+
log.warning("[STATUS] PID encontrado sem heartbeat | pid=%s", pid)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comando stop.
|
|
3
|
+
|
|
4
|
+
Solicita a parada do monitor de risco.
|
|
5
|
+
|
|
6
|
+
O comando cria um arquivo de sinal (`STOP_FILE`) que será
|
|
7
|
+
detectado pelo loop de monitoramento, permitindo uma
|
|
8
|
+
finalização controlada do processo.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from mtcli.logger import setup_logger
|
|
13
|
+
from mtcli.conf import STOP_FILE
|
|
14
|
+
|
|
15
|
+
log = setup_logger("risco")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.command()
|
|
19
|
+
def stop_cmd() -> None:
|
|
20
|
+
"""
|
|
21
|
+
Envia sinal de parada para o monitor de risco.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
with open(STOP_FILE, "w") as f:
|
|
25
|
+
f.write("stop")
|
|
26
|
+
|
|
27
|
+
log.info("[STOP] Sinal de parada enviado")
|
|
28
|
+
|
|
29
|
+
click.echo("Sinal de parada enviado.")
|
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Model de controle de risco.
|
|
3
|
-
|
|
4
|
-
Responsável por:
|
|
5
|
-
- persistência de estado diário
|
|
6
|
-
- verificação de limite de prejuízo
|
|
7
|
-
- encerramento de posições
|
|
8
|
-
- cancelamento de ordens
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import json
|
|
12
|
-
import os
|
|
13
|
-
from datetime import date
|
|
14
|
-
import MetaTrader5 as mt5
|
|
15
|
-
from mtcli.logger import setup_logger
|
|
16
|
-
from mtcli.mt5_context import mt5_conexao
|
|
17
|
-
from .trades_model import calcular_lucro_total_dia
|
|
18
|
-
|
|
19
|
-
log = setup_logger()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def carregar_estado(status_file: str) -> dict:
|
|
23
|
-
"""
|
|
24
|
-
Carrega o estado persistido do controle de risco.
|
|
25
|
-
"""
|
|
26
|
-
if os.path.exists(status_file):
|
|
27
|
-
with open(status_file, "r") as f:
|
|
28
|
-
log.info(f"[ESTADO] Carregando {status_file}")
|
|
29
|
-
return json.load(f)
|
|
30
|
-
return {"data": "", "bloqueado": False}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def salvar_estado(status_file: str, data: date, bloqueado: bool) -> None:
|
|
34
|
-
"""
|
|
35
|
-
Persiste o estado diário do controle de risco.
|
|
36
|
-
"""
|
|
37
|
-
with open(status_file, "w") as f:
|
|
38
|
-
json.dump(
|
|
39
|
-
{"data": data.isoformat(), "bloqueado": bloqueado},
|
|
40
|
-
f,
|
|
41
|
-
indent=2,
|
|
42
|
-
)
|
|
43
|
-
log.info(f"[ESTADO] Salvo | data={data.isoformat()} bloqueado={bloqueado}")
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def encerrar_todas_posicoes() -> None:
|
|
47
|
-
"""
|
|
48
|
-
Encerra todas as posições abertas no MT5.
|
|
49
|
-
"""
|
|
50
|
-
with mt5_conexao():
|
|
51
|
-
positions = mt5.positions_get()
|
|
52
|
-
|
|
53
|
-
if not positions:
|
|
54
|
-
log.info("[MT5] Nenhuma posição aberta.")
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
for pos in positions:
|
|
58
|
-
tipo_oposto = (
|
|
59
|
-
mt5.ORDER_TYPE_SELL
|
|
60
|
-
if pos.type == mt5.ORDER_TYPE_BUY
|
|
61
|
-
else mt5.ORDER_TYPE_BUY
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
ordem = {
|
|
65
|
-
"action": mt5.TRADE_ACTION_DEAL,
|
|
66
|
-
"symbol": pos.symbol,
|
|
67
|
-
"volume": pos.volume,
|
|
68
|
-
"type": tipo_oposto,
|
|
69
|
-
"position": pos.ticket,
|
|
70
|
-
"deviation": 10,
|
|
71
|
-
"magic": 1000,
|
|
72
|
-
"comment": "Fechamento por limite de risco",
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
log.warning(f"[MT5] Encerrando posição {pos.ticket}")
|
|
76
|
-
resultado = mt5.order_send(ordem)
|
|
77
|
-
|
|
78
|
-
if not resultado or resultado.retcode != mt5.TRADE_RETCODE_DONE:
|
|
79
|
-
log.error(f"[MT5] Falha ao fechar {pos.ticket}: {resultado}")
|
|
80
|
-
else:
|
|
81
|
-
log.info(f"[MT5] Posição {pos.ticket} encerrada.")
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def cancelar_todas_ordens() -> None:
|
|
85
|
-
"""
|
|
86
|
-
Cancela todas as ordens pendentes.
|
|
87
|
-
"""
|
|
88
|
-
with mt5_conexao():
|
|
89
|
-
ordens = mt5.orders_get()
|
|
90
|
-
|
|
91
|
-
if not ordens:
|
|
92
|
-
log.info("[MT5] Nenhuma ordem pendente.")
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
for ordem in ordens:
|
|
96
|
-
resultado = mt5.order_delete(ordem.ticket)
|
|
97
|
-
if not resultado or resultado.retcode != mt5.TRADE_RETCODE_DONE:
|
|
98
|
-
log.error(f"[MT5] Falha ao cancelar ordem {ordem.ticket}")
|
|
99
|
-
else:
|
|
100
|
-
log.info(f"[MT5] Ordem {ordem.ticket} cancelada.")
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def risco_excedido(limite: float) -> bool:
|
|
104
|
-
"""
|
|
105
|
-
Verifica se o prejuízo diário ultrapassou o limite configurado.
|
|
106
|
-
"""
|
|
107
|
-
try:
|
|
108
|
-
total = calcular_lucro_total_dia()
|
|
109
|
-
if total <= limite:
|
|
110
|
-
log.warning(
|
|
111
|
-
f"[RISCO] Excedido | resultado={total:.2f} limite={limite:.2f}"
|
|
112
|
-
)
|
|
113
|
-
return True
|
|
114
|
-
return False
|
|
115
|
-
except Exception as exc:
|
|
116
|
-
log.error(f"[RISCO] Erro ao verificar limite: {exc}")
|
|
117
|
-
return False
|
|
1
|
+
"""
|
|
2
|
+
Model de controle de risco.
|
|
3
|
+
|
|
4
|
+
Responsável por:
|
|
5
|
+
- persistência de estado diário
|
|
6
|
+
- verificação de limite de prejuízo
|
|
7
|
+
- encerramento de posições
|
|
8
|
+
- cancelamento de ordens
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from datetime import date
|
|
14
|
+
import MetaTrader5 as mt5
|
|
15
|
+
from mtcli.logger import setup_logger
|
|
16
|
+
from mtcli.mt5_context import mt5_conexao
|
|
17
|
+
from .trades_model import calcular_lucro_total_dia
|
|
18
|
+
|
|
19
|
+
log = setup_logger("risco")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def carregar_estado(status_file: str) -> dict:
|
|
23
|
+
"""
|
|
24
|
+
Carrega o estado persistido do controle de risco.
|
|
25
|
+
"""
|
|
26
|
+
if os.path.exists(status_file):
|
|
27
|
+
with open(status_file, "r") as f:
|
|
28
|
+
log.info(f"[ESTADO] Carregando {status_file}")
|
|
29
|
+
return json.load(f)
|
|
30
|
+
return {"data": "", "bloqueado": False}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def salvar_estado(status_file: str, data: date, bloqueado: bool) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Persiste o estado diário do controle de risco.
|
|
36
|
+
"""
|
|
37
|
+
with open(status_file, "w") as f:
|
|
38
|
+
json.dump(
|
|
39
|
+
{"data": data.isoformat(), "bloqueado": bloqueado},
|
|
40
|
+
f,
|
|
41
|
+
indent=2,
|
|
42
|
+
)
|
|
43
|
+
log.info(f"[ESTADO] Salvo | data={data.isoformat()} bloqueado={bloqueado}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def encerrar_todas_posicoes() -> None:
|
|
47
|
+
"""
|
|
48
|
+
Encerra todas as posições abertas no MT5.
|
|
49
|
+
"""
|
|
50
|
+
with mt5_conexao():
|
|
51
|
+
positions = mt5.positions_get()
|
|
52
|
+
|
|
53
|
+
if not positions:
|
|
54
|
+
log.info("[MT5] Nenhuma posição aberta.")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
for pos in positions:
|
|
58
|
+
tipo_oposto = (
|
|
59
|
+
mt5.ORDER_TYPE_SELL
|
|
60
|
+
if pos.type == mt5.ORDER_TYPE_BUY
|
|
61
|
+
else mt5.ORDER_TYPE_BUY
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
ordem = {
|
|
65
|
+
"action": mt5.TRADE_ACTION_DEAL,
|
|
66
|
+
"symbol": pos.symbol,
|
|
67
|
+
"volume": pos.volume,
|
|
68
|
+
"type": tipo_oposto,
|
|
69
|
+
"position": pos.ticket,
|
|
70
|
+
"deviation": 10,
|
|
71
|
+
"magic": 1000,
|
|
72
|
+
"comment": "Fechamento por limite de risco",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
log.warning(f"[MT5] Encerrando posição {pos.ticket}")
|
|
76
|
+
resultado = mt5.order_send(ordem)
|
|
77
|
+
|
|
78
|
+
if not resultado or resultado.retcode != mt5.TRADE_RETCODE_DONE:
|
|
79
|
+
log.error(f"[MT5] Falha ao fechar {pos.ticket}: {resultado}")
|
|
80
|
+
else:
|
|
81
|
+
log.info(f"[MT5] Posição {pos.ticket} encerrada.")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def cancelar_todas_ordens() -> None:
|
|
85
|
+
"""
|
|
86
|
+
Cancela todas as ordens pendentes.
|
|
87
|
+
"""
|
|
88
|
+
with mt5_conexao():
|
|
89
|
+
ordens = mt5.orders_get()
|
|
90
|
+
|
|
91
|
+
if not ordens:
|
|
92
|
+
log.info("[MT5] Nenhuma ordem pendente.")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
for ordem in ordens:
|
|
96
|
+
resultado = mt5.order_delete(ordem.ticket)
|
|
97
|
+
if not resultado or resultado.retcode != mt5.TRADE_RETCODE_DONE:
|
|
98
|
+
log.error(f"[MT5] Falha ao cancelar ordem {ordem.ticket}")
|
|
99
|
+
else:
|
|
100
|
+
log.info(f"[MT5] Ordem {ordem.ticket} cancelada.")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def risco_excedido(limite: float) -> bool:
|
|
104
|
+
"""
|
|
105
|
+
Verifica se o prejuízo diário ultrapassou o limite configurado.
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
total = calcular_lucro_total_dia()
|
|
109
|
+
if total <= limite:
|
|
110
|
+
log.warning(
|
|
111
|
+
f"[RISCO] Excedido | resultado={total:.2f} limite={limite:.2f}"
|
|
112
|
+
)
|
|
113
|
+
return True
|
|
114
|
+
return False
|
|
115
|
+
except Exception as exc:
|
|
116
|
+
log.error(f"[RISCO] Erro ao verificar limite: {exc}")
|
|
117
|
+
return False
|