Make ticks dataset opt-in for collect-history (#87)
* feat: make SQLite tick history opt-in for collect-history
Ticks can grow SQLite databases quickly, so they are excluded from the
default dataset selection. The new DEFAULT_HISTORY_DATASETS constant
(rates, history-orders, history-deals) drives resolve_history_datasets(None),
collect_history(), and update_history(). Callers must pass
--dataset ticks (CLI) or datasets={Dataset.ticks} (SDK) to include ticks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ALK71tg75JWrrCKaiShb7b
* chore: reformat docs/index.md table column widths
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ALK71tg75JWrrCKaiShb7b
* fix: update stale docstrings and tighten CLI None check
- update_history and ThrottledHistoryUpdater.__init__ docstrings now
state that ticks are opt-in, matching collect_history's wording
- cli.py collect-history uses `is not None` for explicit empty-list safety
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ALK71tg75JWrrCKaiShb7b
* docs: update README collect-history to reflect ticks opt-in default
The command table and section intro previously stated ticks were
collected by default ("all four", "rates, ticks, history-orders, and
history-deals"). Both now reflect the new default (rates, history-orders,
history-deals) and note that --dataset ticks is required to include ticks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ALK71tg75JWrrCKaiShb7b
---------
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
+43
-6
@@ -1298,12 +1298,12 @@ class TestCollectHistory:
|
||||
"""Create a mocked Mt5DataClient with history-style DataFrames."""
|
||||
return _build_history_client(mocker)
|
||||
|
||||
def test_collect_history_writes_all_tables(
|
||||
def test_collect_history_writes_default_tables(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
history_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test that collect-history writes rates, ticks, and history tables."""
|
||||
"""Test that collect-history default excludes ticks."""
|
||||
output = tmp_path / "history.db"
|
||||
result = runner.invoke(
|
||||
app,
|
||||
@@ -1323,8 +1323,42 @@ class TestCollectHistory:
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
assert history_client.copy_rates_range_as_df.call_count == 2
|
||||
assert history_client.copy_ticks_range_as_df.call_count == 2
|
||||
history_client.copy_ticks_range_as_df.assert_any_call(
|
||||
assert history_client.copy_ticks_range_as_df.call_count == 0
|
||||
with sqlite3.connect(output) as conn:
|
||||
tables = {
|
||||
row[0]
|
||||
for row in conn.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table'",
|
||||
).fetchall()
|
||||
}
|
||||
assert {"rates", "history_orders", "history_deals"} <= tables
|
||||
assert "ticks" not in tables
|
||||
|
||||
def test_collect_history_explicit_ticks_dataset(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
history_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test that --dataset ticks writes the ticks table with the correct flags."""
|
||||
output = tmp_path / "history.db"
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"-o",
|
||||
str(output),
|
||||
"collect-history",
|
||||
"--symbol",
|
||||
"EURUSD",
|
||||
"--date-from",
|
||||
"2024-01-01",
|
||||
"--date-to",
|
||||
"2024-02-01",
|
||||
"--dataset",
|
||||
"ticks",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
history_client.copy_ticks_range_as_df.assert_called_once_with(
|
||||
symbol="EURUSD",
|
||||
date_from=datetime(2024, 1, 1, tzinfo=UTC),
|
||||
date_to=datetime(2024, 2, 1, tzinfo=UTC),
|
||||
@@ -1337,7 +1371,8 @@ class TestCollectHistory:
|
||||
"SELECT name FROM sqlite_master WHERE type='table'",
|
||||
).fetchall()
|
||||
}
|
||||
assert {"rates", "ticks", "history_orders", "history_deals"} <= tables
|
||||
assert "ticks" in tables
|
||||
assert "rates" not in tables
|
||||
|
||||
def test_collect_history_history_fetched_per_symbol(
|
||||
self,
|
||||
@@ -1520,7 +1555,7 @@ class TestCollectHistory:
|
||||
tmp_path: Path,
|
||||
history_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test that --flags defaults to ALL for ticks."""
|
||||
"""Test that --flags defaults to ALL when --dataset ticks is explicit."""
|
||||
output = tmp_path / "history.db"
|
||||
result = runner.invoke(
|
||||
app,
|
||||
@@ -1534,6 +1569,8 @@ class TestCollectHistory:
|
||||
"2024-01-01",
|
||||
"--date-to",
|
||||
"2024-02-01",
|
||||
"--dataset",
|
||||
"ticks",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
+16
-2
@@ -19,6 +19,7 @@ from pdmt5 import TIMEFRAME_MAP
|
||||
|
||||
from mt5cli import history
|
||||
from mt5cli.history import (
|
||||
DEFAULT_HISTORY_DATASETS,
|
||||
DEFAULT_HISTORY_TIMEFRAMES,
|
||||
DedupScope,
|
||||
RateTarget,
|
||||
@@ -547,10 +548,23 @@ class TestResolveHistorySettings:
|
||||
"""Tests for history dataset and timeframe resolution."""
|
||||
|
||||
def test_resolve_history_datasets_defaults_and_empty(self) -> None:
|
||||
"""Test dataset resolution distinguishes None from empty selection."""
|
||||
assert resolve_history_datasets(None) == set(Dataset)
|
||||
"""Test dataset resolution excludes ticks by default."""
|
||||
resolved = resolve_history_datasets(None)
|
||||
assert resolved == set(DEFAULT_HISTORY_DATASETS)
|
||||
assert Dataset.ticks not in resolved
|
||||
assert {
|
||||
Dataset.rates,
|
||||
Dataset.history_orders,
|
||||
Dataset.history_deals,
|
||||
} == resolved
|
||||
assert resolve_history_datasets(set()) == set()
|
||||
|
||||
def test_resolve_history_datasets_explicit_ticks(self) -> None:
|
||||
"""Test that explicit ticks selection is honored."""
|
||||
assert resolve_history_datasets({Dataset.ticks}) == {Dataset.ticks}
|
||||
all_ds = resolve_history_datasets(set(Dataset))
|
||||
assert Dataset.ticks in all_ds
|
||||
|
||||
def test_resolve_history_timeframes_defaults(self) -> None:
|
||||
"""Test default timeframes include all fixed MT5 values."""
|
||||
resolved = resolve_history_timeframes(None)
|
||||
|
||||
+65
-4
@@ -618,12 +618,12 @@ class TestCollectHistory:
|
||||
"""Create a mocked Mt5DataClient with history-style DataFrames."""
|
||||
return _build_history_client(mocker)
|
||||
|
||||
def test_collect_history_writes_all_tables(
|
||||
def test_collect_history_writes_default_tables(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
history_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test that collect_history writes rates, ticks, and history tables."""
|
||||
"""Test that collect_history default excludes ticks."""
|
||||
output = tmp_path / "history.db"
|
||||
collect_history(
|
||||
output,
|
||||
@@ -632,7 +632,7 @@ class TestCollectHistory:
|
||||
"2024-02-01",
|
||||
)
|
||||
assert history_client.copy_rates_range_as_df.call_count == 2
|
||||
assert history_client.copy_ticks_range_as_df.call_count == 2
|
||||
assert history_client.copy_ticks_range_as_df.call_count == 0
|
||||
with sqlite3.connect(output) as conn:
|
||||
tables = {
|
||||
row[0]
|
||||
@@ -640,7 +640,34 @@ class TestCollectHistory:
|
||||
"SELECT name FROM sqlite_master WHERE type='table'",
|
||||
).fetchall()
|
||||
}
|
||||
assert {"rates", "ticks", "history_orders", "history_deals"} <= tables
|
||||
assert {"rates", "history_orders", "history_deals"} <= tables
|
||||
assert "ticks" not in tables
|
||||
|
||||
def test_collect_history_explicit_ticks_dataset(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
history_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test that explicit datasets={Dataset.ticks} writes the ticks table."""
|
||||
output = tmp_path / "history.db"
|
||||
collect_history(
|
||||
output,
|
||||
["EURUSD", "GBPUSD"],
|
||||
"2024-01-01",
|
||||
"2024-02-01",
|
||||
datasets={Dataset.ticks},
|
||||
)
|
||||
assert history_client.copy_ticks_range_as_df.call_count == 2
|
||||
assert history_client.copy_rates_range_as_df.call_count == 0
|
||||
with sqlite3.connect(output) as conn:
|
||||
tables = {
|
||||
row[0]
|
||||
for row in conn.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table'",
|
||||
).fetchall()
|
||||
}
|
||||
assert "ticks" in tables
|
||||
assert "rates" not in tables
|
||||
|
||||
def test_collect_history_with_views(
|
||||
self,
|
||||
@@ -1066,6 +1093,40 @@ class TestUpdateHistory:
|
||||
after = datetime.now(UTC)
|
||||
assert before <= captured["end"] <= after
|
||||
|
||||
def test_update_history_default_datasets_exclude_ticks(
|
||||
self,
|
||||
connected_client: MagicMock,
|
||||
mocker: MockerFixture,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test update_history with datasets=None does not collect ticks."""
|
||||
datasets_written: list[set[Dataset]] = []
|
||||
|
||||
def capture(
|
||||
*args: object,
|
||||
**_kwargs: object,
|
||||
) -> tuple[set[Dataset], dict[Dataset, set[str]]]:
|
||||
datasets_written.append(args[3]) # type: ignore[arg-type]
|
||||
return set(), {}
|
||||
|
||||
mocker.patch("mt5cli.sdk.write_incremental_datasets", side_effect=capture)
|
||||
update_history(
|
||||
client=connected_client,
|
||||
output=tmp_path / "default-datasets.db",
|
||||
symbols=["EURUSD"],
|
||||
datasets=None,
|
||||
timeframes=["M1"],
|
||||
lookback_hours=1,
|
||||
date_to=datetime(2024, 1, 1, tzinfo=UTC),
|
||||
)
|
||||
assert len(datasets_written) == 1
|
||||
assert Dataset.ticks not in datasets_written[0]
|
||||
assert {
|
||||
Dataset.rates,
|
||||
Dataset.history_orders,
|
||||
Dataset.history_deals,
|
||||
} == datasets_written[0]
|
||||
|
||||
|
||||
class TestRecentTicks:
|
||||
"""Tests for recent_ticks helper."""
|
||||
|
||||
Reference in New Issue
Block a user