mtcli 3.8.0.dev15__tar.gz → 3.8.0.dev16__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.
Files changed (105) hide show
  1. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/PKG-INFO +2 -1
  2. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/__main__.py +3 -3
  3. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/cli.py +17 -37
  4. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/cli_dev.py +8 -8
  5. mtcli-3.8.0.dev16/mtcli/commands/backfill.py +70 -0
  6. mtcli-3.8.0.dev16/mtcli/commands/ticks.py +58 -0
  7. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/commands_dev/migrate.py +48 -48
  8. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/conf.py +16 -0
  9. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/database.py +137 -137
  10. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/logger.py +178 -136
  11. mtcli-3.8.0.dev15/mtcli/marketdata/backfill.py → mtcli-3.8.0.dev16/mtcli/marketdata/backfill_engine.py +136 -166
  12. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/marketdata/tick_bus.py +41 -61
  13. mtcli-3.8.0.dev16/mtcli/marketdata/tick_engine.py +87 -0
  14. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/marketdata/tick_repository.py +12 -9
  15. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/marketdata/tick_writer.py +44 -44
  16. mtcli-3.8.0.dev16/mtcli/marketdata/trade_tick_filter.py +39 -0
  17. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/migrations/001_initial_schema.py +51 -51
  18. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/migrations/002_ticks_time_msc.py +53 -53
  19. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/migrations/003_optimize_ticks_without_rowid.py +86 -86
  20. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/migrations/004_ticks_pk_time_msc.py +78 -78
  21. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/migrations/005_tick_price_compression.py +86 -86
  22. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/migrations/006_add_index_ticks_symbol_time.py +32 -32
  23. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/migrations/007_deduplicate_ticks_and_add_unique_index.py +91 -91
  24. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/migrations/008_fix_ticks_unique_index.py +98 -98
  25. mtcli-3.8.0.dev16/mtcli/migrations/009_keep_only_trade_ticks.py +84 -0
  26. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/migrations/__main__.py +7 -7
  27. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/migrations/runner.py +301 -301
  28. mtcli-3.8.0.dev16/mtcli/services/tick_service.py +56 -0
  29. mtcli-3.8.0.dev16/mtcli/utils/__init__.py +0 -0
  30. mtcli-3.8.0.dev16/mtcli/utils/pidfile.py +35 -0
  31. mtcli-3.8.0.dev16/mtcli/utils/time.py +16 -0
  32. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/pyproject.toml +2 -1
  33. mtcli-3.8.0.dev15/mtcli/commands/backfill.py +0 -70
  34. mtcli-3.8.0.dev15/mtcli/commands/ticks.py +0 -44
  35. mtcli-3.8.0.dev15/mtcli/marketdata/tick_engine.py +0 -151
  36. mtcli-3.8.0.dev15/mtcli/services/tick_service.py +0 -82
  37. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/LICENSE +0 -0
  38. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/README.md +0 -0
  39. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/__init__.py +0 -0
  40. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/commands/__init__.py +0 -0
  41. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/commands/bars.py +0 -0
  42. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/commands/conf.py +0 -0
  43. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/commands/doctor.py +0 -0
  44. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/commands_dev/__init__.py +0 -0
  45. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/conecta.py +0 -0
  46. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/config_registre.py +0 -0
  47. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/data/__init__.py +0 -0
  48. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/data/base.py +0 -0
  49. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/data/csv.py +0 -0
  50. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/data/mt5.py +0 -0
  51. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/domain/__init__.py +0 -0
  52. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/domain/timeframe.py +0 -0
  53. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/marketdata/__init__.py +0 -0
  54. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/marketdata/tick_cache.py +0 -0
  55. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/migrations/__init__.py +0 -0
  56. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/models/__init__.py +0 -0
  57. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/models/bar_model.py +0 -0
  58. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/models/bars_model.py +0 -0
  59. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/models/chart_model.py +0 -0
  60. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/models/conf_model.py +0 -0
  61. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/models/consecutive_bars_model.py +0 -0
  62. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/models/rates_model.py +0 -0
  63. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/models/signals_model.py +0 -0
  64. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/models/unconsecutive_bar_model.py +0 -0
  65. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/mt5_context.py +0 -0
  66. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugin.py +0 -0
  67. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugin_loader.py +0 -0
  68. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugin_manager.py +0 -0
  69. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/__init__.py +0 -0
  70. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/exemplo.py-dist +0 -0
  71. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/media_movel/__init__.py +0 -0
  72. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/media_movel/cli.py +0 -0
  73. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/media_movel/conf.py +0 -0
  74. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/media_movel/models/__init__.py +0 -0
  75. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/media_movel/models/model_media_movel.py +0 -0
  76. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/media_movel/tests/__init__.py +0 -0
  77. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/media_movel/tests/test_mm.py +0 -0
  78. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/media_movel/tests/test_model_media_movel.py +0 -0
  79. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/range_medio/__init__.py +0 -0
  80. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/range_medio/cli.py +0 -0
  81. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/range_medio/conf.py +0 -0
  82. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/range_medio/models/__init__.py +0 -0
  83. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/range_medio/models/average_range_model.py +0 -0
  84. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/range_medio/tests/__init__.py +0 -0
  85. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/range_medio/tests/test_rm.py +0 -0
  86. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/volume_medio/__init__.py +0 -0
  87. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/volume_medio/cli.py +0 -0
  88. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/volume_medio/conf.py +0 -0
  89. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/volume_medio/models/__init__.py +0 -0
  90. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/volume_medio/models/model_average_volume.py +0 -0
  91. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/volume_medio/tests/__init__.py +0 -0
  92. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/plugins/volume_medio/tests/test_vm.py +0 -0
  93. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/services/__init__.py +0 -0
  94. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/services/maintenance_service.py +0 -0
  95. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/views/__init__.py +0 -0
  96. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/views/close_view.py +0 -0
  97. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/views/full_view.py +0 -0
  98. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/views/high_view.py +0 -0
  99. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/views/low_view.py +0 -0
  100. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/views/min_view.py +0 -0
  101. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/views/open_view.py +0 -0
  102. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/views/ranges_view.py +0 -0
  103. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/views/rates_view.py +0 -0
  104. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/views/vars_view.py +0 -0
  105. {mtcli-3.8.0.dev15 → mtcli-3.8.0.dev16}/mtcli/views/volumes_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mtcli
3
- Version: 3.8.0.dev15
3
+ Version: 3.8.0.dev16
4
4
  Summary: Aplicativo CLI para exibir gráficos do MetaTrader 5 screen reader friendly
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -19,6 +19,7 @@ Classifier: Operating System :: OS Independent
19
19
  Classifier: Topic :: Office/Business :: Financial :: Investment
20
20
  Requires-Dist: click (>=8.2.1,<9.0.0)
21
21
  Requires-Dist: metatrader5 (>=5.0.5260,<6.0.0)
22
+ Requires-Dist: tzdata (>=2025.3,<2026.0)
22
23
  Project-URL: Documentation, https://mtcli.readthedocs.io/pt-br/latest
23
24
  Project-URL: Homepage, https://github.com:vfranca/mtcli
24
25
  Project-URL: Repository, https://github.com/vfranca/mtcli
@@ -1,3 +1,3 @@
1
- from .cli import mt
2
-
3
- mt()
1
+ from .cli import mt
2
+
3
+ mt()
@@ -1,26 +1,9 @@
1
1
  """
2
2
  CLI principal do mtcli.
3
-
4
- Responsável por:
5
-
6
- - inicializar o ambiente do CLI
7
- - carregar plugins
8
- - iniciar captura automática de ticks (opcional)
9
-
10
- A captura automática é ativada se a variável de ambiente
11
- MTCLI_SYMBOL estiver definida.
12
-
13
- Exemplo:
14
-
15
- MTCLI_SYMBOL=WIN$N mt bars
16
-
17
- Nesse caso o mtcli inicia um TickEngine em background
18
- para manter o histórico de ticks atualizado.
19
3
  """
20
4
 
21
5
  import os
22
6
  import click
23
-
24
7
  from mtcli.plugin_loader import load_plugins
25
8
  from mtcli.logger import setup_logger
26
9
  from mtcli.services.tick_service import ensure_tick_engine
@@ -30,43 +13,42 @@ from .commands.doctor import doctor
30
13
  from .commands.ticks import ticks
31
14
  from .commands.backfill import backfill
32
15
 
33
-
34
16
  logger = setup_logger(__name__)
35
17
 
36
- _tick_engine = None
18
+ _tick_engines = []
37
19
 
38
20
 
39
21
  def start_tick_capture():
40
22
  """
41
- Inicia captura contínua de ticks em background.
42
-
43
- Se a variável de ambiente MTCLI_SYMBOL estiver definida,
44
- o mtcli iniciará automaticamente um TickEngine para
45
- coletar ticks continuamente.
46
-
47
- Isso permite manter um histórico próprio de ticks
48
- independente do histórico do broker.
23
+ Inicia captura contínua de ticks em background se MTCLI_SYMBOL estiver definido.
49
24
  """
50
25
 
51
- global _tick_engine
26
+ global _tick_engines
52
27
 
53
- if _tick_engine:
28
+ if _tick_engines:
54
29
  return
55
30
 
56
- symbol = os.getenv("MTCLI_SYMBOL")
31
+ symbols = os.getenv("MTCLI_SYMBOL")
57
32
 
58
- if not symbol:
33
+ if not symbols:
59
34
  logger.info(
60
35
  "Captura automática de ticks desativada (MTCLI_SYMBOL não definido)."
61
36
  )
62
37
  return
63
38
 
64
- logger.info("Iniciando captura contínua de ticks para %s", symbol)
39
+ if isinstance(symbols, str):
40
+ symbols = [symbols]
65
41
 
66
- try:
42
+ logger.info("Iniciando captura contínua de ticks para %s", symbols)
67
43
 
68
- # CORREÇÃO PRINCIPAL
69
- _tick_engine = ensure_tick_engine([symbol])
44
+ try:
45
+ for symbol in symbols:
46
+ engine = ensure_tick_engine(symbol)
47
+ _tick_engines.append(engine)
48
+ # pode rodar em thread se quiser background
49
+ import threading
50
+ t = threading.Thread(target=engine.start, daemon=True)
51
+ t.start()
70
52
 
71
53
  logger.info("Captura de ticks iniciada em background.")
72
54
 
@@ -95,7 +77,6 @@ mt.add_command(ticks)
95
77
  mt.add_command(backfill, name="fill")
96
78
 
97
79
  loaded_plugins = load_plugins(mt)
98
-
99
80
  logger.info("Plugins carregados: %s", loaded_plugins)
100
81
 
101
82
 
@@ -110,7 +91,6 @@ def list_plugins():
110
91
  return
111
92
 
112
93
  click.echo("Plugins carregados:\n")
113
-
114
94
  for name in loaded_plugins:
115
95
  click.echo(f" {name}")
116
96
 
@@ -1,8 +1,8 @@
1
- import click
2
- from .commands_dev.migrate import migrate
3
-
4
- @click.group()
5
- def cli():
6
- pass
7
-
8
- cli.add_command(migrate)
1
+ import click
2
+ from .commands_dev.migrate import migrate
3
+
4
+ @click.group()
5
+ def cli():
6
+ pass
7
+
8
+ cli.add_command(migrate)
@@ -0,0 +1,70 @@
1
+ """
2
+ Comando CLI: backfill
3
+
4
+ Carrega histórico de ticks do MetaTrader5
5
+ para o banco SQLite do mtcli utilizando o BackfillEngine
6
+ com filtro de trade ticks.
7
+
8
+ Fluxo:
9
+
10
+ BackfillEngine
11
+ ->
12
+ raw_tick_bus
13
+ ->
14
+ TradeTickFilter
15
+ ->
16
+ trade_tick_bus
17
+ ->
18
+ TickWriter / plugins
19
+ ->
20
+ TickRepository
21
+ ->
22
+ SQLite
23
+ """
24
+
25
+ import click
26
+
27
+ from mtcli.logger import setup_logger
28
+ from mtcli.marketdata.tick_bus import TickBus
29
+ from mtcli.marketdata.tick_repository import TickRepository
30
+ from mtcli.marketdata.tick_writer import TickWriter
31
+ from mtcli.marketdata.trade_tick_filter import TradeTickFilter
32
+ from mtcli.marketdata.backfill_engine import BackfillEngine
33
+
34
+ logger = setup_logger(__name__)
35
+
36
+
37
+ @click.command()
38
+ @click.argument("symbol")
39
+ @click.option(
40
+ "--days",
41
+ default=5,
42
+ show_default=True,
43
+ )
44
+ def backfill(symbol: str, days: int):
45
+
46
+ raw_tick_bus = TickBus()
47
+ trade_tick_bus = TickBus()
48
+
49
+ repo = TickRepository()
50
+
51
+ raw_tick_bus.subscribe(
52
+ TradeTickFilter(trade_tick_bus)
53
+ )
54
+
55
+ writer = TickWriter(symbol, repo)
56
+
57
+ trade_tick_bus.subscribe(writer)
58
+
59
+ engine = BackfillEngine(
60
+ symbol,
61
+ raw_tick_bus,
62
+ repo,
63
+ )
64
+
65
+ engine.run(days)
66
+
67
+ logger.info(
68
+ "Backfill concluído (%s) — trade ticks gravados",
69
+ symbol,
70
+ )
@@ -0,0 +1,58 @@
1
+ """
2
+ Comando CLI para captura contínua de ticks.
3
+
4
+ Exemplo:
5
+ mt ticks WIN$N
6
+ mt ticks WIN$N WDO$N PETR4
7
+ """
8
+
9
+ import click
10
+ import threading
11
+
12
+ from mtcli.services.tick_service import ensure_tick_engine
13
+ from mtcli.logger import setup_logger
14
+
15
+ logger = setup_logger(__name__)
16
+
17
+
18
+ @click.command()
19
+ @click.argument("symbols", nargs=-1)
20
+ def ticks(symbols):
21
+
22
+ if not symbols:
23
+
24
+ click.echo("Informe ao menos um símbolo.")
25
+ return
26
+
27
+ engines = []
28
+
29
+ for symbol in symbols:
30
+
31
+ engine = ensure_tick_engine(symbol)
32
+
33
+ t = threading.Thread(
34
+ target=engine.start,
35
+ daemon=True
36
+ )
37
+
38
+ t.start()
39
+
40
+ engines.append(engine)
41
+
42
+ click.echo(
43
+ f"Captura de ticks iniciada para {symbol}"
44
+ )
45
+
46
+ try:
47
+
48
+ while True:
49
+
50
+ for engine in engines:
51
+ pass
52
+
53
+ except KeyboardInterrupt:
54
+
55
+ click.echo("\nEncerrando captura...")
56
+
57
+ for engine in engines:
58
+ engine.stop()
@@ -1,48 +1,48 @@
1
- """
2
- Comando CLI para execução das migrations do banco de dados.
3
-
4
- Este comando aplica todas as migrations pendentes no banco
5
- utilizado pelo mtcli.
6
-
7
- Fluxo de execução:
8
-
9
- 1. Abre conexão com o banco SQLite
10
- 2. Executa o migration runner
11
- 3. Aplica migrations ainda não executadas
12
-
13
- Uso:
14
-
15
- mtcli migrate
16
-
17
- Este comando é implementado utilizando o framework
18
- :contentReference[oaicite:1]{index=1} para construção de aplicações CLI.
19
- """
20
-
21
- import click
22
-
23
- from mtcli.database import get_connection
24
- from mtcli.migrations.runner import run_migrations
25
-
26
-
27
- @click.command()
28
- def migrate():
29
- """
30
- Executa as migrations pendentes do banco de dados.
31
-
32
- O comando conecta ao banco configurado no mtcli
33
- e executa o migration runner responsável por:
34
-
35
- - detectar migrations disponíveis
36
- - identificar a versão atual do schema
37
- - aplicar migrations pendentes
38
- - registrar migrations aplicadas
39
-
40
- Este comando é normalmente executado:
41
-
42
- - na primeira inicialização do sistema
43
- - após atualização de versão do mtcli
44
- """
45
-
46
- conn = get_connection()
47
-
48
- run_migrations(conn)
1
+ """
2
+ Comando CLI para execução das migrations do banco de dados.
3
+
4
+ Este comando aplica todas as migrations pendentes no banco
5
+ utilizado pelo mtcli.
6
+
7
+ Fluxo de execução:
8
+
9
+ 1. Abre conexão com o banco SQLite
10
+ 2. Executa o migration runner
11
+ 3. Aplica migrations ainda não executadas
12
+
13
+ Uso:
14
+
15
+ mtcli migrate
16
+
17
+ Este comando é implementado utilizando o framework
18
+ :contentReference[oaicite:1]{index=1} para construção de aplicações CLI.
19
+ """
20
+
21
+ import click
22
+
23
+ from mtcli.database import get_connection
24
+ from mtcli.migrations.runner import run_migrations
25
+
26
+
27
+ @click.command()
28
+ def migrate():
29
+ """
30
+ Executa as migrations pendentes do banco de dados.
31
+
32
+ O comando conecta ao banco configurado no mtcli
33
+ e executa o migration runner responsável por:
34
+
35
+ - detectar migrations disponíveis
36
+ - identificar a versão atual do schema
37
+ - aplicar migrations pendentes
38
+ - registrar migrations aplicadas
39
+
40
+ Este comando é normalmente executado:
41
+
42
+ - na primeira inicialização do sistema
43
+ - após atualização de versão do mtcli
44
+ """
45
+
46
+ conn = get_connection()
47
+
48
+ run_migrations(conn)
@@ -313,3 +313,19 @@ _INITIAL_CSV_PATH = conf.get_csv_path()
313
313
  # ---------------------------------------------------------
314
314
 
315
315
  DB_NAME = conf.get("db_name", default="marketdata.db")
316
+
317
+ # ---------------------------------------------------------
318
+ # gestão de múltiplos processos
319
+ # ---------------------------------------------------------
320
+
321
+ RUN_DIR = os.path.join(
322
+ os.getenv("APPDATA", os.path.expanduser("~")),
323
+ "mtcli",
324
+ "run"
325
+ )
326
+
327
+ os.makedirs(RUN_DIR, exist_ok=True)
328
+
329
+ PID_FILE = os.path.join(RUN_DIR, "risco.pid")
330
+ STOP_FILE = os.path.join(RUN_DIR, "risco.stop")
331
+ HEARTBEAT_FILE = os.path.join(RUN_DIR, "risco.heartbeat")
@@ -1,137 +1,137 @@
1
- """
2
- Core de acesso ao banco SQLite do mtcli.
3
-
4
- Responsável por:
5
-
6
- - criar conexão SQLite
7
- - aplicar otimizações
8
- - executar migrations automaticamente
9
- - backup automático
10
- """
11
-
12
- import sqlite3
13
- from pathlib import Path
14
- from datetime import datetime
15
-
16
- from .conf import DB_NAME
17
- from .migrations.runner import run_migrations
18
- from .logger import setup_logger
19
-
20
-
21
- # ==========================================================
22
- # LOGGER
23
- # ==========================================================
24
-
25
- log = setup_logger(__name__)
26
-
27
-
28
- # ==========================================================
29
- # PATHS
30
- # ==========================================================
31
-
32
- DB_PATH = Path.home() / ".mtcli" / DB_NAME
33
- BACKUP_DIR = Path.home() / ".mtcli" / "backups"
34
-
35
- _connection = None
36
-
37
-
38
- # ==========================================================
39
- # CONNECTION
40
- # ==========================================================
41
-
42
- def get_connection():
43
- """
44
- Retorna conexão singleton SQLite.
45
- """
46
-
47
- global _connection
48
-
49
- if _connection:
50
- log.debug("Reutilizando conexão SQLite existente.")
51
- return _connection
52
-
53
- log.debug("Inicializando conexão SQLite.")
54
-
55
- DB_PATH.parent.mkdir(parents=True, exist_ok=True)
56
- BACKUP_DIR.mkdir(parents=True, exist_ok=True)
57
-
58
- log.debug("Diretórios garantidos: db=%s backups=%s", DB_PATH.parent, BACKUP_DIR)
59
-
60
- conn = sqlite3.connect(DB_PATH, check_same_thread=False)
61
-
62
- log.debug("Conexão SQLite criada em %s", DB_PATH)
63
-
64
- # ======================================================
65
- # PRAGMAS DE OTIMIZAÇÃO
66
- # ======================================================
67
-
68
- log.debug("Aplicando otimizações PRAGMA.")
69
-
70
- conn.execute("PRAGMA journal_mode=WAL")
71
- conn.execute("PRAGMA synchronous=NORMAL")
72
- conn.execute("PRAGMA temp_store=MEMORY")
73
- conn.execute("PRAGMA mmap_size=30000000000")
74
- conn.execute("PRAGMA cache_size=-200000")
75
- conn.execute("PRAGMA journal_size_limit=67108864")
76
- conn.execute("PRAGMA wal_autocheckpoint=5000")
77
- conn.execute("PRAGMA foreign_keys=ON")
78
-
79
- log.debug("PRAGMAs aplicadas com sucesso.")
80
-
81
- # ======================================================
82
- # MIGRATIONS
83
- # ======================================================
84
-
85
- log.debug("Executando migrations do banco de dados.")
86
-
87
- try:
88
- run_migrations(conn)
89
- log.info("Migrations executadas/verificadas com sucesso.")
90
- except Exception:
91
- log.exception("Erro ao executar migrations.")
92
- raise
93
-
94
- _connection = conn
95
-
96
- log.debug("Conexão SQLite inicializada e armazenada como singleton.")
97
-
98
- return conn
99
-
100
-
101
- # ==========================================================
102
- # BACKUP
103
- # ==========================================================
104
-
105
- def backup_database(conn):
106
- """
107
- Cria backup diário do banco de dados SQLite.
108
-
109
- O backup é realizado apenas uma vez por dia.
110
- """
111
-
112
- now = datetime.now().strftime("%Y%m%d")
113
-
114
- backup_path = BACKUP_DIR / f"marketdata_{now}.db"
115
-
116
- log.debug("Verificando necessidade de backup diário: %s", backup_path)
117
-
118
- if backup_path.exists():
119
- log.debug("Backup diário já existe. Ignorando backup.")
120
- return
121
-
122
- log.info("Iniciando backup do banco para %s", backup_path)
123
-
124
- try:
125
-
126
- backup_conn = sqlite3.connect(backup_path)
127
-
128
- with backup_conn:
129
- conn.backup(backup_conn)
130
-
131
- backup_conn.close()
132
-
133
- log.info("Backup concluído com sucesso.")
134
-
135
- except Exception:
136
- log.exception("Falha durante backup do banco.")
137
- raise
1
+ """
2
+ Core de acesso ao banco SQLite do mtcli.
3
+
4
+ Responsável por:
5
+
6
+ - criar conexão SQLite
7
+ - aplicar otimizações
8
+ - executar migrations automaticamente
9
+ - backup automático
10
+ """
11
+
12
+ import sqlite3
13
+ from pathlib import Path
14
+ from datetime import datetime
15
+
16
+ from .conf import DB_NAME
17
+ from .migrations.runner import run_migrations
18
+ from .logger import setup_logger
19
+
20
+
21
+ # ==========================================================
22
+ # LOGGER
23
+ # ==========================================================
24
+
25
+ log = setup_logger(__name__)
26
+
27
+
28
+ # ==========================================================
29
+ # PATHS
30
+ # ==========================================================
31
+
32
+ DB_PATH = Path.home() / ".mtcli" / DB_NAME
33
+ BACKUP_DIR = Path.home() / ".mtcli" / "backups"
34
+
35
+ _connection = None
36
+
37
+
38
+ # ==========================================================
39
+ # CONNECTION
40
+ # ==========================================================
41
+
42
+ def get_connection():
43
+ """
44
+ Retorna conexão singleton SQLite.
45
+ """
46
+
47
+ global _connection
48
+
49
+ if _connection:
50
+ log.debug("Reutilizando conexão SQLite existente.")
51
+ return _connection
52
+
53
+ log.debug("Inicializando conexão SQLite.")
54
+
55
+ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
56
+ BACKUP_DIR.mkdir(parents=True, exist_ok=True)
57
+
58
+ log.debug("Diretórios garantidos: db=%s backups=%s", DB_PATH.parent, BACKUP_DIR)
59
+
60
+ conn = sqlite3.connect(DB_PATH, check_same_thread=False)
61
+
62
+ log.debug("Conexão SQLite criada em %s", DB_PATH)
63
+
64
+ # ======================================================
65
+ # PRAGMAS DE OTIMIZAÇÃO
66
+ # ======================================================
67
+
68
+ log.debug("Aplicando otimizações PRAGMA.")
69
+
70
+ conn.execute("PRAGMA journal_mode=WAL")
71
+ conn.execute("PRAGMA synchronous=NORMAL")
72
+ conn.execute("PRAGMA temp_store=MEMORY")
73
+ conn.execute("PRAGMA mmap_size=30000000000")
74
+ conn.execute("PRAGMA cache_size=-200000")
75
+ conn.execute("PRAGMA journal_size_limit=67108864")
76
+ conn.execute("PRAGMA wal_autocheckpoint=5000")
77
+ conn.execute("PRAGMA foreign_keys=ON")
78
+
79
+ log.debug("PRAGMAs aplicadas com sucesso.")
80
+
81
+ # ======================================================
82
+ # MIGRATIONS
83
+ # ======================================================
84
+
85
+ log.debug("Executando migrations do banco de dados.")
86
+
87
+ try:
88
+ run_migrations(conn)
89
+ log.info("Migrations executadas/verificadas com sucesso.")
90
+ except Exception:
91
+ log.exception("Erro ao executar migrations.")
92
+ raise
93
+
94
+ _connection = conn
95
+
96
+ log.debug("Conexão SQLite inicializada e armazenada como singleton.")
97
+
98
+ return conn
99
+
100
+
101
+ # ==========================================================
102
+ # BACKUP
103
+ # ==========================================================
104
+
105
+ def backup_database(conn):
106
+ """
107
+ Cria backup diário do banco de dados SQLite.
108
+
109
+ O backup é realizado apenas uma vez por dia.
110
+ """
111
+
112
+ now = datetime.now().strftime("%Y%m%d")
113
+
114
+ backup_path = BACKUP_DIR / f"marketdata_{now}.db"
115
+
116
+ log.debug("Verificando necessidade de backup diário: %s", backup_path)
117
+
118
+ if backup_path.exists():
119
+ log.debug("Backup diário já existe. Ignorando backup.")
120
+ return
121
+
122
+ log.info("Iniciando backup do banco para %s", backup_path)
123
+
124
+ try:
125
+
126
+ backup_conn = sqlite3.connect(backup_path)
127
+
128
+ with backup_conn:
129
+ conn.backup(backup_conn)
130
+
131
+ backup_conn.close()
132
+
133
+ log.info("Backup concluído com sucesso.")
134
+
135
+ except Exception:
136
+ log.exception("Falha durante backup do banco.")
137
+ raise