mtcli 3.8.0.dev7__tar.gz → 3.8.0.dev9__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 (92) hide show
  1. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/PKG-INFO +1 -1
  2. mtcli-3.8.0.dev9/mtcli/__main__.py +3 -0
  3. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/cli.py +0 -2
  4. mtcli-3.8.0.dev9/mtcli/cli_dev.py +8 -0
  5. mtcli-3.8.0.dev9/mtcli/commands_dev/migrate.py +48 -0
  6. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/database.py +1 -1
  7. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/marketdata/tick_repository.py +2 -3
  8. mtcli-3.8.0.dev9/mtcli/migrations/001_initial_schema.py +51 -0
  9. mtcli-3.8.0.dev9/mtcli/migrations/002_ticks_time_msc.py +53 -0
  10. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/migrations/003_optimize_ticks_without_rowid.py +23 -0
  11. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/migrations/004_ticks_pk_time_msc.py +22 -0
  12. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/migrations/005_tick_price_compression.py +27 -0
  13. mtcli-3.8.0.dev9/mtcli/migrations/006_add_index_ticks_symbol_time.py +32 -0
  14. mtcli-3.8.0.dev9/mtcli/migrations/007_deduplicate_ticks_and_add_unique_index.py +91 -0
  15. mtcli-3.8.0.dev9/mtcli/migrations/__main__.py +7 -0
  16. mtcli-3.8.0.dev9/mtcli/migrations/runner.py +245 -0
  17. mtcli-3.8.0.dev9/mtcli/plugins/volume_medio/tests/__init__.py +0 -0
  18. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/pyproject.toml +2 -1
  19. mtcli-3.8.0.dev7/mtcli/commands/migrate.py +0 -12
  20. mtcli-3.8.0.dev7/mtcli/migrations/001_initial_schema.py +0 -21
  21. mtcli-3.8.0.dev7/mtcli/migrations/002_ticks_time_msc.py +0 -21
  22. mtcli-3.8.0.dev7/mtcli/migrations/runner.py +0 -104
  23. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/LICENSE +0 -0
  24. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/README.md +0 -0
  25. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/__init__.py +0 -0
  26. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/commands/__init__.py +0 -0
  27. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/commands/bars.py +0 -0
  28. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/commands/conf.py +0 -0
  29. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/commands/doctor.py +0 -0
  30. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/commands/ticks.py +0 -0
  31. {mtcli-3.8.0.dev7/mtcli/domain → mtcli-3.8.0.dev9/mtcli/commands_dev}/__init__.py +0 -0
  32. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/conecta.py +0 -0
  33. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/conf.py +0 -0
  34. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/config_registre.py +0 -0
  35. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/data/__init__.py +0 -0
  36. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/data/base.py +0 -0
  37. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/data/csv.py +0 -0
  38. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/data/mt5.py +0 -0
  39. {mtcli-3.8.0.dev7/mtcli/marketdata → mtcli-3.8.0.dev9/mtcli/domain}/__init__.py +0 -0
  40. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/domain/timeframe.py +0 -0
  41. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/logger.py +0 -0
  42. {mtcli-3.8.0.dev7/mtcli/migrations → mtcli-3.8.0.dev9/mtcli/marketdata}/__init__.py +0 -0
  43. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/marketdata/tick_cache.py +0 -0
  44. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/marketdata/tick_engine.py +0 -0
  45. {mtcli-3.8.0.dev7/mtcli/plugins → mtcli-3.8.0.dev9/mtcli/migrations}/__init__.py +0 -0
  46. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/models/__init__.py +0 -0
  47. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/models/bar_model.py +0 -0
  48. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/models/bars_model.py +0 -0
  49. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/models/chart_model.py +0 -0
  50. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/models/conf_model.py +0 -0
  51. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/models/consecutive_bars_model.py +0 -0
  52. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/models/rates_model.py +0 -0
  53. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/models/signals_model.py +0 -0
  54. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/models/unconsecutive_bar_model.py +0 -0
  55. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/mt5_context.py +0 -0
  56. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugin.py +0 -0
  57. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugin_loader.py +0 -0
  58. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugin_manager.py +0 -0
  59. {mtcli-3.8.0.dev7/mtcli/plugins/media_movel/tests → mtcli-3.8.0.dev9/mtcli/plugins}/__init__.py +0 -0
  60. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/exemplo.py-dist +0 -0
  61. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/media_movel/__init__.py +0 -0
  62. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/media_movel/cli.py +0 -0
  63. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/media_movel/conf.py +0 -0
  64. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/media_movel/models/__init__.py +0 -0
  65. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/media_movel/models/model_media_movel.py +0 -0
  66. {mtcli-3.8.0.dev7/mtcli/plugins/range_medio/models → mtcli-3.8.0.dev9/mtcli/plugins/media_movel/tests}/__init__.py +0 -0
  67. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/media_movel/tests/test_mm.py +0 -0
  68. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/media_movel/tests/test_model_media_movel.py +0 -0
  69. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/range_medio/__init__.py +0 -0
  70. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/range_medio/cli.py +0 -0
  71. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/range_medio/conf.py +0 -0
  72. {mtcli-3.8.0.dev7/mtcli/plugins/range_medio/tests → mtcli-3.8.0.dev9/mtcli/plugins/range_medio/models}/__init__.py +0 -0
  73. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/range_medio/models/average_range_model.py +0 -0
  74. {mtcli-3.8.0.dev7/mtcli/plugins/volume_medio → mtcli-3.8.0.dev9/mtcli/plugins/range_medio}/tests/__init__.py +0 -0
  75. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/range_medio/tests/test_rm.py +0 -0
  76. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/volume_medio/__init__.py +0 -0
  77. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/volume_medio/cli.py +0 -0
  78. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/volume_medio/conf.py +0 -0
  79. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/volume_medio/models/__init__.py +0 -0
  80. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/volume_medio/models/model_average_volume.py +0 -0
  81. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/plugins/volume_medio/tests/test_vm.py +0 -0
  82. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/views/__init__.py +0 -0
  83. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/views/close_view.py +0 -0
  84. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/views/full_view.py +0 -0
  85. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/views/high_view.py +0 -0
  86. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/views/low_view.py +0 -0
  87. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/views/min_view.py +0 -0
  88. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/views/open_view.py +0 -0
  89. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/views/ranges_view.py +0 -0
  90. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/views/rates_view.py +0 -0
  91. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/mtcli/views/vars_view.py +0 -0
  92. {mtcli-3.8.0.dev7 → mtcli-3.8.0.dev9}/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.dev7
3
+ Version: 3.8.0.dev9
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
@@ -0,0 +1,3 @@
1
+ from .cli import mt
2
+
3
+ mt()
@@ -27,7 +27,6 @@ from mtcli.marketdata.tick_engine import TickEngine
27
27
 
28
28
  from .commands.bars import bars
29
29
  from .commands.doctor import doctor
30
- from .commands.migrate import migrate
31
30
  from .commands.ticks import ticks
32
31
 
33
32
 
@@ -89,7 +88,6 @@ def mt(ctx):
89
88
  mt.add_command(doctor, name="doctor")
90
89
  mt.add_command(bars, name="bars")
91
90
  mt.add_command(doctor, name="dr")
92
- mt.add_command(migrate)
93
91
  mt.add_command(ticks)
94
92
 
95
93
  loaded_plugins = load_plugins(mt)
@@ -0,0 +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)
@@ -0,0 +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)
@@ -41,7 +41,7 @@ def get_connection():
41
41
  conn.execute("PRAGMA journal_mode=WAL")
42
42
  conn.execute("PRAGMA synchronous=NORMAL")
43
43
  conn.execute("PRAGMA temp_store=MEMORY")
44
- conn.execute("PRAGMA mmap_size=268435456")
44
+ conn.execute("PRAGMA mmap_size=30000000000")
45
45
  conn.execute("PRAGMA cache_size=-200000")
46
46
  conn.execute("PRAGMA journal_size_limit=67108864")
47
47
  conn.execute("PRAGMA read_uncommitted = TRUE")
@@ -114,7 +114,7 @@ class TickRepository:
114
114
 
115
115
  cursor.executemany(
116
116
  """
117
- INSERT INTO ticks(
117
+ INSERT OR IGNORE INTO ticks(
118
118
  symbol,
119
119
  time_msc,
120
120
  bid,
@@ -124,12 +124,11 @@ class TickRepository:
124
124
  flags
125
125
  )
126
126
  VALUES (?,?,?,?,?,?,?)
127
- ON CONFLICT(symbol,time_msc) DO NOTHING
128
127
  """,
129
128
  data,
130
129
  )
131
130
 
132
- return len(data)
131
+ return cursor.rowcount
133
132
 
134
133
  # ==========================================================
135
134
  # CONSULTAS
@@ -0,0 +1,51 @@
1
+ """
2
+ Migration 001
3
+
4
+ Cria o schema inicial do banco de dados do mtcli.
5
+
6
+ Esta migration cria a tabela principal `ticks`, responsável por
7
+ armazenar os ticks de mercado capturados do MetaTrader.
8
+
9
+ Estrutura inicial:
10
+
11
+ - symbol : símbolo do ativo
12
+ - time : timestamp em segundos
13
+ - bid : preço bid
14
+ - ask : preço ask
15
+ - last : último preço negociado
16
+ - volume : volume do tick
17
+ - flags : flags fornecidas pela API MT5
18
+
19
+ Também cria o índice `idx_ticks_symbol_time` para acelerar
20
+ consultas por símbolo e tempo.
21
+ """
22
+
23
+
24
+ def upgrade(conn):
25
+ """
26
+ Executa a migration inicial criando a tabela `ticks`
27
+ e o índice principal utilizado nas consultas.
28
+
29
+ A operação é idempotente graças ao uso de
30
+ `CREATE TABLE IF NOT EXISTS`.
31
+ """
32
+
33
+ conn.execute("""
34
+ CREATE TABLE IF NOT EXISTS ticks(
35
+ symbol TEXT NOT NULL,
36
+ time INTEGER NOT NULL,
37
+ bid REAL,
38
+ ask REAL,
39
+ last REAL,
40
+ volume REAL,
41
+ flags INTEGER,
42
+ PRIMARY KEY(symbol, time)
43
+ )
44
+ """)
45
+
46
+ conn.execute("""
47
+ CREATE INDEX IF NOT EXISTS idx_ticks_symbol_time
48
+ ON ticks(symbol, time)
49
+ """)
50
+
51
+ conn.commit()
@@ -0,0 +1,53 @@
1
+ """
2
+ Migration 002
3
+
4
+ Adiciona a coluna `time_msc` à tabela `ticks`.
5
+
6
+ A API do MetaTrader fornece timestamps com resolução em
7
+ milissegundos (`time_msc`). Esta migration adiciona suporte
8
+ a esse campo sem quebrar compatibilidade com bancos antigos.
9
+ """
10
+
11
+
12
+ def column_exists(conn, table, column):
13
+ """
14
+ Verifica se uma coluna existe em uma tabela SQLite.
15
+
16
+ Parameters
17
+ ----------
18
+ conn : sqlite3.Connection
19
+ Conexão com o banco de dados.
20
+ table : str
21
+ Nome da tabela.
22
+ column : str
23
+ Nome da coluna.
24
+
25
+ Returns
26
+ -------
27
+ bool
28
+ True se a coluna existir.
29
+ """
30
+
31
+ cursor = conn.execute(f"PRAGMA table_info({table})")
32
+
33
+ for row in cursor.fetchall():
34
+ if row[1] == column:
35
+ return True
36
+
37
+ return False
38
+
39
+
40
+ def upgrade(conn):
41
+ """
42
+ Adiciona a coluna `time_msc` à tabela `ticks`
43
+ caso ela ainda não exista.
44
+ """
45
+
46
+ if not column_exists(conn, "ticks", "time_msc"):
47
+
48
+ conn.execute("""
49
+ ALTER TABLE ticks
50
+ ADD COLUMN time_msc INTEGER
51
+ """)
52
+
53
+ conn.commit()
@@ -1,4 +1,27 @@
1
+ """
2
+ Migration 003
3
+
4
+ Otimiza a tabela `ticks` convertendo-a para `WITHOUT ROWID`.
5
+
6
+ Tabelas SQLite com chave primária composta podem ter melhor
7
+ performance e menor uso de disco utilizando `WITHOUT ROWID`.
8
+
9
+ Para aplicar esta otimização é necessário reconstruir a tabela.
10
+ """
11
+
12
+
1
13
  def upgrade(conn):
14
+ """
15
+ Reconstrói a tabela `ticks` utilizando `WITHOUT ROWID`.
16
+
17
+ Processo executado:
18
+
19
+ 1. Cria nova tabela otimizada
20
+ 2. Copia os dados da tabela antiga
21
+ 3. Remove tabela antiga
22
+ 4. Renomeia nova tabela
23
+ 5. Recria índices necessários
24
+ """
2
25
 
3
26
  cursor = conn.execute("""
4
27
  SELECT sql
@@ -1,4 +1,26 @@
1
+ """
2
+ Migration 004
3
+
4
+ Reconstrói a tabela `ticks` utilizando `time_msc` como parte
5
+ da chave primária.
6
+
7
+ Motivação:
8
+
9
+ - `time` possui resolução em segundos
10
+ - `time_msc` possui resolução em milissegundos
11
+ - múltiplos ticks podem ocorrer dentro do mesmo segundo
12
+
13
+ Nova chave primária:
14
+
15
+ PRIMARY KEY(symbol, time_msc)
16
+ """
17
+
18
+
1
19
  def upgrade(conn):
20
+ """
21
+ Reconstrói a tabela `ticks` alterando a chave primária
22
+ para `(symbol, time_msc)`.
23
+ """
2
24
 
3
25
  cursor = conn.execute("""
4
26
  SELECT sql
@@ -1,4 +1,31 @@
1
+ """
2
+ Migration 005
3
+
4
+ Aplica compressão de preços nos ticks.
5
+
6
+ Preços e volume passam a ser armazenados como inteiros
7
+ para reduzir uso de espaço em disco.
8
+
9
+ Transformações aplicadas:
10
+
11
+ bid -> INTEGER (bid * 100)
12
+ ask -> INTEGER (ask * 100)
13
+ last -> INTEGER (last * 100)
14
+ volume -> INTEGER
15
+
16
+ A tabela precisa ser reconstruída para alterar os tipos
17
+ das colunas.
18
+ """
19
+
20
+
1
21
  def upgrade(conn):
22
+ """
23
+ Reconstrói a tabela `ticks` aplicando compressão
24
+ de preços e volumes.
25
+
26
+ Isso reduz significativamente o tamanho do banco
27
+ e melhora performance de leitura.
28
+ """
2
29
 
3
30
  cursor = conn.execute("""
4
31
  SELECT sql
@@ -0,0 +1,32 @@
1
+ """
2
+ Migration 006
3
+
4
+ Cria índice para acelerar consultas por símbolo e tempo
5
+ utilizando a coluna `time_msc`.
6
+
7
+ Este índice é importante para consultas comuns como:
8
+
9
+ SELECT * FROM ticks
10
+ WHERE symbol = ?
11
+ ORDER BY time_msc
12
+ """
13
+
14
+ SQL = """
15
+ CREATE INDEX IF NOT EXISTS idx_ticks_symbol_time
16
+ ON ticks(symbol, time_msc);
17
+ """
18
+
19
+
20
+ def upgrade(conn):
21
+ """
22
+ Executa a criação do índice `idx_ticks_symbol_time`.
23
+
24
+ A operação é idempotente graças ao uso de
25
+ `CREATE INDEX IF NOT EXISTS`.
26
+ """
27
+
28
+ cursor = conn.cursor()
29
+
30
+ cursor.execute(SQL)
31
+
32
+ conn.commit()
@@ -0,0 +1,91 @@
1
+ """
2
+ Migration 007
3
+
4
+ Objetivo
5
+ --------
6
+
7
+ 1. Remover registros duplicados da tabela `ticks`
8
+ 2. Criar índice UNIQUE para evitar duplicação futura
9
+
10
+ Critério de unicidade adotado:
11
+
12
+ (symbol, time_msc)
13
+
14
+ Estratégia
15
+ ----------
16
+
17
+ 1. Criar tabela temporária com SELECT DISTINCT
18
+ 2. Copiar dados únicos
19
+ 3. Substituir tabela original
20
+ 4. Criar índice UNIQUE
21
+
22
+ Essa estratégia é muito mais rápida do que DELETE em tabelas grandes.
23
+ """
24
+
25
+ from sqlite3 import Connection
26
+
27
+
28
+ def upgrade(conn: Connection) -> None:
29
+ cursor = conn.cursor()
30
+
31
+ # ---------------------------------------------------------
32
+ # 1 - criar nova tabela sem duplicados
33
+ # ---------------------------------------------------------
34
+
35
+ cursor.execute(
36
+ """
37
+ CREATE TABLE IF NOT EXISTS ticks_new AS
38
+ SELECT DISTINCT *
39
+ FROM ticks
40
+ """
41
+ )
42
+
43
+ # ---------------------------------------------------------
44
+ # 2 - remover tabela antiga
45
+ # ---------------------------------------------------------
46
+
47
+ cursor.execute("DROP TABLE ticks")
48
+
49
+ # ---------------------------------------------------------
50
+ # 3 - renomear tabela nova
51
+ # ---------------------------------------------------------
52
+
53
+ cursor.execute("ALTER TABLE ticks_new RENAME TO ticks")
54
+
55
+ # ---------------------------------------------------------
56
+ # 4 - recriar índice de performance existente
57
+ # ---------------------------------------------------------
58
+
59
+ cursor.execute(
60
+ """
61
+ CREATE INDEX IF NOT EXISTS idx_ticks_symbol_time
62
+ ON ticks(symbol, time_msc)
63
+ """
64
+ )
65
+
66
+ # ---------------------------------------------------------
67
+ # 5 - criar índice UNIQUE
68
+ # ---------------------------------------------------------
69
+
70
+ cursor.execute(
71
+ """
72
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_ticks_unique
73
+ ON ticks(symbol, time_msc)
74
+ """
75
+ )
76
+
77
+ conn.commit()
78
+
79
+
80
+ def downgrade(conn: Connection) -> None:
81
+ cursor = conn.cursor()
82
+
83
+ # Remove índice UNIQUE caso exista rollback
84
+
85
+ cursor.execute(
86
+ """
87
+ DROP INDEX IF EXISTS idx_ticks_unique
88
+ """
89
+ )
90
+
91
+ conn.commit()
@@ -0,0 +1,7 @@
1
+ from mtcli.database import get_connection
2
+ from .runner import run_migrations
3
+
4
+
5
+ conn = get_connection()
6
+ run_migrations(conn)
7
+
@@ -0,0 +1,245 @@
1
+ """
2
+ Migration Runner do mtcli.
3
+
4
+ Responsabilidades:
5
+
6
+ 1. Descobrir migrations disponíveis no diretório `mtcli/migrations`
7
+ 2. Determinar a versão atual do schema no banco
8
+ 3. Executar migrations pendentes em ordem
9
+ 4. Registrar migrations aplicadas na tabela `schema_migrations`
10
+
11
+ Formato obrigatório das migrations:
12
+
13
+ NNN_nome_da_migration.py
14
+
15
+ Exemplo:
16
+
17
+ 001_initial_schema.py
18
+ 002_ticks_time_msc.py
19
+ 003_optimize_ticks_without_rowid.py
20
+
21
+ Cada migration deve implementar:
22
+
23
+ def upgrade(conn)
24
+ """
25
+
26
+ import importlib
27
+ from pathlib import Path
28
+
29
+
30
+ MIGRATIONS_DIR = Path(__file__).parent
31
+ PACKAGE = "mtcli.migrations"
32
+
33
+
34
+ # ---------------------------------------------------------
35
+ # Infraestrutura
36
+ # ---------------------------------------------------------
37
+
38
+ def ensure_migrations_table(conn):
39
+ """
40
+ Garante que a tabela `schema_migrations` exista.
41
+
42
+ Esta tabela armazena quais migrations já foram
43
+ aplicadas no banco.
44
+ """
45
+
46
+ conn.execute("""
47
+ CREATE TABLE IF NOT EXISTS schema_migrations(
48
+ version INTEGER PRIMARY KEY,
49
+ applied_at TEXT NOT NULL
50
+ )
51
+ """)
52
+
53
+ conn.commit()
54
+
55
+
56
+ # ---------------------------------------------------------
57
+ # Estado do banco
58
+ # ---------------------------------------------------------
59
+
60
+ def get_current_version(conn):
61
+ """
62
+ Retorna a versão atual do schema.
63
+
64
+ Returns
65
+ -------
66
+ int
67
+ Maior versão registrada em `schema_migrations`.
68
+ Retorna 0 se nenhuma migration foi aplicada.
69
+ """
70
+
71
+ cursor = conn.execute(
72
+ "SELECT MAX(version) FROM schema_migrations"
73
+ )
74
+
75
+ row = cursor.fetchone()
76
+
77
+ return row[0] if row and row[0] else 0
78
+
79
+
80
+ def mark_version(conn, version):
81
+ """
82
+ Marca uma migration como aplicada.
83
+ """
84
+
85
+ conn.execute(
86
+ """
87
+ INSERT INTO schema_migrations(version, applied_at)
88
+ VALUES (?, datetime('now'))
89
+ """,
90
+ (version,),
91
+ )
92
+
93
+ conn.commit()
94
+
95
+
96
+ # ---------------------------------------------------------
97
+ # Descoberta de migrations
98
+ # ---------------------------------------------------------
99
+
100
+ def discover_migrations():
101
+ """
102
+ Descobre migrations válidas no diretório.
103
+
104
+ Apenas arquivos no formato:
105
+
106
+ NNN_nome.py
107
+
108
+ serão considerados migrations.
109
+
110
+ Returns
111
+ -------
112
+ list[(int,str)]
113
+ """
114
+
115
+ migrations = []
116
+
117
+ for file in MIGRATIONS_DIR.glob("*.py"):
118
+
119
+ if file.name in ("__init__.py", "runner.py"):
120
+ continue
121
+
122
+ prefix = file.stem.split("_")[0]
123
+
124
+ # ignora arquivos inválidos
125
+ if not prefix.isdigit():
126
+ continue
127
+
128
+ version = int(prefix)
129
+
130
+ module_name = file.stem
131
+
132
+ migrations.append((version, module_name))
133
+
134
+ migrations.sort(key=lambda x: x[0])
135
+
136
+ validate_migrations(migrations)
137
+
138
+ return migrations
139
+
140
+
141
+ def validate_migrations(migrations):
142
+ """
143
+ Valida integridade das migrations.
144
+
145
+ Verifica:
146
+
147
+ - versões duplicadas
148
+ - gaps na sequência
149
+ """
150
+
151
+ versions = [v for v, _ in migrations]
152
+
153
+ if len(versions) != len(set(versions)):
154
+ raise RuntimeError("Duplicate migration versions detected.")
155
+
156
+ if not versions:
157
+ return
158
+
159
+ expected = list(range(min(versions), max(versions) + 1))
160
+
161
+ if versions != expected:
162
+ raise RuntimeError(
163
+ f"Migration sequence gap detected: {versions}"
164
+ )
165
+
166
+
167
+ # ---------------------------------------------------------
168
+ # Compatibilidade com bancos antigos
169
+ # ---------------------------------------------------------
170
+
171
+ def legacy_database_detected(conn):
172
+ """
173
+ Detecta bancos antigos sem controle de migrations.
174
+ """
175
+
176
+ cursor = conn.execute("""
177
+ SELECT name
178
+ FROM sqlite_master
179
+ WHERE type='table'
180
+ AND name='ticks'
181
+ """)
182
+
183
+ return cursor.fetchone() is not None
184
+
185
+
186
+ def bootstrap_legacy(conn):
187
+ """
188
+ Inicializa migrations em banco legado.
189
+ """
190
+
191
+ if legacy_database_detected(conn):
192
+
193
+ conn.execute("""
194
+ INSERT OR IGNORE INTO schema_migrations(version, applied_at)
195
+ VALUES (1, datetime('now'))
196
+ """)
197
+
198
+ conn.commit()
199
+
200
+
201
+ # ---------------------------------------------------------
202
+ # Runner principal
203
+ # ---------------------------------------------------------
204
+
205
+ def run_migrations(conn):
206
+ """
207
+ Executa migrations pendentes.
208
+
209
+ Fluxo:
210
+
211
+ 1. Garante tabela `schema_migrations`
212
+ 2. Detecta bancos legados
213
+ 3. Descobre migrations
214
+ 4. Executa migrations pendentes
215
+ """
216
+
217
+ ensure_migrations_table(conn)
218
+
219
+ bootstrap_legacy(conn)
220
+
221
+ current = get_current_version(conn)
222
+
223
+ migrations = discover_migrations()
224
+
225
+ for version, module_name in migrations:
226
+
227
+ if version <= current:
228
+ continue
229
+
230
+ module_path = f"{PACKAGE}.{module_name}"
231
+
232
+ module = importlib.import_module(module_path)
233
+
234
+ if not hasattr(module, "upgrade"):
235
+ raise RuntimeError(
236
+ f"Migration {module_name} does not define upgrade()"
237
+ )
238
+
239
+ print(f"Applying migration {version}: {module_name}")
240
+
241
+ module.upgrade(conn)
242
+
243
+ mark_version(conn, version)
244
+
245
+ print("Migrations concluídas.")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mtcli"
3
- version = "3.8.0.dev7"
3
+ version = "3.8.0.dev9"
4
4
  description = "Aplicativo CLI para exibir gráficos do MetaTrader 5 screen reader friendly"
5
5
  authors = [
6
6
  {name = "Valmir França",email = "vfranca3@gmail.com"}
@@ -45,6 +45,7 @@ issues = "https://github.com/vfranca/mtcli/issues"
45
45
  [project.scripts]
46
46
  mtcli = "mtcli.cli:mt"
47
47
  mt = "mtcli.cli:mt"
48
+ mtdev = "mtcli.cli_dev:cli"
48
49
 
49
50
  [project.entry-points."mtcli.plugins"]
50
51
  internals = "mtcli.plugin:register"
@@ -1,12 +0,0 @@
1
- import click
2
-
3
- from mtcli.database import get_connection
4
- from mtcli.migrations.runner import run_migrations
5
-
6
-
7
- @click.command()
8
- def migrate():
9
-
10
- conn = get_connection()
11
-
12
- run_migrations(conn)
@@ -1,21 +0,0 @@
1
- def upgrade(conn):
2
-
3
- conn.execute("""
4
- CREATE TABLE IF NOT EXISTS ticks(
5
- symbol TEXT NOT NULL,
6
- time INTEGER NOT NULL,
7
- bid REAL,
8
- ask REAL,
9
- last REAL,
10
- volume REAL,
11
- flags INTEGER,
12
- PRIMARY KEY(symbol, time)
13
- )
14
- """)
15
-
16
- conn.execute("""
17
- CREATE INDEX IF NOT EXISTS idx_ticks_symbol_time
18
- ON ticks(symbol, time)
19
- """)
20
-
21
- conn.commit()
@@ -1,21 +0,0 @@
1
- def column_exists(conn, table, column):
2
-
3
- cursor = conn.execute(f"PRAGMA table_info({table})")
4
-
5
- for row in cursor.fetchall():
6
- if row[1] == column:
7
- return True
8
-
9
- return False
10
-
11
-
12
- def upgrade(conn):
13
-
14
- if not column_exists(conn, "ticks", "time_msc"):
15
-
16
- conn.execute("""
17
- ALTER TABLE ticks
18
- ADD COLUMN time_msc INTEGER
19
- """)
20
-
21
- conn.commit()
@@ -1,104 +0,0 @@
1
- import importlib
2
- from pathlib import Path
3
-
4
-
5
- MIGRATIONS_DIR = Path(__file__).parent
6
- PACKAGE = "mtcli.migrations"
7
-
8
-
9
- def get_current_version(conn):
10
-
11
- cursor = conn.execute(
12
- "SELECT MAX(version) FROM schema_migrations"
13
- )
14
-
15
- row = cursor.fetchone()
16
-
17
- return row[0] if row and row[0] else 0
18
-
19
-
20
- def mark_version(conn, version):
21
-
22
- conn.execute(
23
- """
24
- INSERT INTO schema_migrations(version, applied_at)
25
- VALUES (?, datetime('now'))
26
- """,
27
- (version,),
28
- )
29
-
30
- conn.commit()
31
-
32
-
33
- def discover_migrations():
34
-
35
- migrations = []
36
-
37
- for file in MIGRATIONS_DIR.glob("*.py"):
38
-
39
- if file.name in ("__init__.py", "runner.py"):
40
- continue
41
-
42
- version = int(file.name.split("_")[0])
43
-
44
- module_name = file.stem
45
-
46
- migrations.append((version, module_name))
47
-
48
- migrations.sort(key=lambda x: x[0])
49
-
50
- return migrations
51
-
52
-
53
- def legacy_database_detected(conn):
54
-
55
- cursor = conn.execute("""
56
- SELECT name
57
- FROM sqlite_master
58
- WHERE type='table'
59
- AND name='ticks'
60
- """)
61
-
62
- return cursor.fetchone() is not None
63
-
64
-
65
- def bootstrap_legacy(conn):
66
-
67
- """
68
- Marca migration 1 como aplicada se banco já tinha schema.
69
- """
70
-
71
- if legacy_database_detected(conn):
72
-
73
- conn.execute("""
74
- INSERT OR IGNORE INTO schema_migrations(version, applied_at)
75
- VALUES (1, datetime('now'))
76
- """)
77
-
78
- conn.commit()
79
-
80
-
81
- def run_migrations(conn):
82
-
83
- bootstrap_legacy(conn)
84
-
85
- current = get_current_version(conn)
86
-
87
- migrations = discover_migrations()
88
-
89
- for version, module_name in migrations:
90
-
91
- if version <= current:
92
- continue
93
-
94
- module_path = f"{PACKAGE}.{module_name}"
95
-
96
- module = importlib.import_module(module_path)
97
-
98
- print(f"Applying migration {version}: {module_name}")
99
-
100
- module.upgrade(conn)
101
-
102
- mark_version(conn, version)
103
-
104
- print("Migrations concluídas.")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes