Add interactive visualizer using Plotly and Dash, replacing the static matplotlib implementation. Introduced core modules for Dash app setup, custom components, and callback functions. Enhanced data processing utilities for Plotly format integration and updated dependencies in pyproject.toml.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Tests for SQLiteMetricsRepository table creation and schema validation."""
|
||||
"""Tests for SQLiteOrderflowRepository table creation and schema validation."""
|
||||
|
||||
import sys
|
||||
import sqlite3
|
||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
|
||||
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from repositories.sqlite_metrics_repository import SQLiteMetricsRepository
|
||||
from repositories.sqlite_repository import SQLiteOrderflowRepository
|
||||
from models import Metric
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ def test_create_metrics_table():
|
||||
db_path = Path(tmp_file.name)
|
||||
|
||||
try:
|
||||
repo = SQLiteMetricsRepository(db_path)
|
||||
repo = SQLiteOrderflowRepository(db_path)
|
||||
with repo.connect() as conn:
|
||||
# Create metrics table
|
||||
repo.create_metrics_table(conn)
|
||||
@@ -54,7 +54,7 @@ def test_insert_metrics_batch():
|
||||
db_path = Path(tmp_file.name)
|
||||
|
||||
try:
|
||||
repo = SQLiteMetricsRepository(db_path)
|
||||
repo = SQLiteOrderflowRepository(db_path)
|
||||
with repo.connect() as conn:
|
||||
# Create metrics table
|
||||
repo.create_metrics_table(conn)
|
||||
@@ -94,7 +94,7 @@ def test_load_metrics_by_timerange():
|
||||
db_path = Path(tmp_file.name)
|
||||
|
||||
try:
|
||||
repo = SQLiteMetricsRepository(db_path)
|
||||
repo = SQLiteOrderflowRepository(db_path)
|
||||
with repo.connect() as conn:
|
||||
# Create metrics table and insert test data
|
||||
repo.create_metrics_table(conn)
|
||||
|
||||
@@ -9,7 +9,7 @@ from datetime import datetime
|
||||
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from storage import Storage
|
||||
from repositories.sqlite_metrics_repository import SQLiteMetricsRepository
|
||||
from repositories.sqlite_repository import SQLiteOrderflowRepository
|
||||
|
||||
|
||||
def test_storage_calculates_and_stores_metrics():
|
||||
@@ -60,13 +60,13 @@ def test_storage_calculates_and_stores_metrics():
|
||||
storage.build_booktick_from_db(db_path, datetime.now())
|
||||
|
||||
# Verify metrics were calculated and stored
|
||||
metrics_repo = SQLiteMetricsRepository(db_path)
|
||||
with metrics_repo.connect() as conn:
|
||||
repo = SQLiteOrderflowRepository(db_path)
|
||||
with repo.connect() as conn:
|
||||
# Check metrics table exists
|
||||
assert metrics_repo.table_exists(conn, "metrics")
|
||||
assert repo.table_exists(conn, "metrics")
|
||||
|
||||
# Load calculated metrics
|
||||
metrics = metrics_repo.load_metrics_by_timerange(conn, 1000, 1000)
|
||||
metrics = repo.load_metrics_by_timerange(conn, 1000, 1000)
|
||||
assert len(metrics) == 1
|
||||
|
||||
metric = metrics[0]
|
||||
|
||||
@@ -9,7 +9,7 @@ sys.path.append(str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from strategies import DefaultStrategy
|
||||
from models import Book, BookSnapshot, OrderbookLevel, Metric
|
||||
from repositories.sqlite_metrics_repository import SQLiteMetricsRepository
|
||||
from repositories.sqlite_repository import SQLiteOrderflowRepository
|
||||
|
||||
|
||||
def test_strategy_uses_metric_calculator():
|
||||
@@ -41,9 +41,9 @@ def test_strategy_loads_stored_metrics():
|
||||
|
||||
try:
|
||||
# Create test database with metrics
|
||||
metrics_repo = SQLiteMetricsRepository(db_path)
|
||||
with metrics_repo.connect() as conn:
|
||||
metrics_repo.create_metrics_table(conn)
|
||||
repo = SQLiteOrderflowRepository(db_path)
|
||||
with repo.connect() as conn:
|
||||
repo.create_metrics_table(conn)
|
||||
|
||||
# Insert test metrics
|
||||
test_metrics = [
|
||||
@@ -52,7 +52,7 @@ def test_strategy_loads_stored_metrics():
|
||||
Metric(snapshot_id=3, timestamp=1002, obi=0.3, cvd=20.0, best_bid=50004.0, best_ask=50005.0),
|
||||
]
|
||||
|
||||
metrics_repo.insert_metrics_batch(conn, test_metrics)
|
||||
repo.insert_metrics_batch(conn, test_metrics)
|
||||
conn.commit()
|
||||
|
||||
# Test strategy loading
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
"""Tests for Visualizer metrics integration."""
|
||||
|
||||
import sys
|
||||
import sqlite3
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from visualizer import Visualizer
|
||||
from models import Book, BookSnapshot, OrderbookLevel, Metric
|
||||
from repositories.sqlite_metrics_repository import SQLiteMetricsRepository
|
||||
|
||||
|
||||
def test_visualizer_loads_metrics():
|
||||
"""Test that visualizer can load stored metrics from database."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp_file:
|
||||
db_path = Path(tmp_file.name)
|
||||
|
||||
try:
|
||||
# Create test database with metrics
|
||||
metrics_repo = SQLiteMetricsRepository(db_path)
|
||||
with metrics_repo.connect() as conn:
|
||||
metrics_repo.create_metrics_table(conn)
|
||||
|
||||
# Insert test metrics
|
||||
test_metrics = [
|
||||
Metric(snapshot_id=1, timestamp=1000, obi=0.1, cvd=10.0, best_bid=50000.0, best_ask=50001.0),
|
||||
Metric(snapshot_id=2, timestamp=1060, obi=0.2, cvd=15.0, best_bid=50002.0, best_ask=50003.0),
|
||||
Metric(snapshot_id=3, timestamp=1120, obi=-0.1, cvd=12.0, best_bid=50004.0, best_ask=50005.0),
|
||||
]
|
||||
|
||||
metrics_repo.insert_metrics_batch(conn, test_metrics)
|
||||
conn.commit()
|
||||
|
||||
# Test visualizer
|
||||
visualizer = Visualizer(window_seconds=60, max_bars=200)
|
||||
visualizer.set_db_path(db_path)
|
||||
|
||||
# Load metrics directly to test the method
|
||||
loaded_metrics = visualizer._load_stored_metrics(1000, 1120)
|
||||
|
||||
assert len(loaded_metrics) == 3
|
||||
assert loaded_metrics[0].obi == 0.1
|
||||
assert loaded_metrics[0].cvd == 10.0
|
||||
assert loaded_metrics[1].obi == 0.2
|
||||
assert loaded_metrics[2].obi == -0.1
|
||||
|
||||
finally:
|
||||
db_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def test_visualizer_handles_no_database():
|
||||
"""Test that visualizer handles gracefully when no database path is set."""
|
||||
visualizer = Visualizer(window_seconds=60, max_bars=200)
|
||||
|
||||
# No database path set - should return empty list
|
||||
metrics = visualizer._load_stored_metrics(1000, 2000)
|
||||
assert metrics == []
|
||||
|
||||
|
||||
def test_visualizer_handles_invalid_database():
|
||||
"""Test that visualizer handles invalid database paths gracefully."""
|
||||
visualizer = Visualizer(window_seconds=60, max_bars=200)
|
||||
visualizer.set_db_path(Path("nonexistent.db"))
|
||||
|
||||
# Should handle error gracefully and return empty list
|
||||
metrics = visualizer._load_stored_metrics(1000, 2000)
|
||||
assert metrics == []
|
||||
|
||||
|
||||
@patch('matplotlib.pyplot.subplots')
|
||||
def test_visualizer_creates_four_subplots(mock_subplots):
|
||||
"""Test that visualizer creates four subplots for OHLC, Volume, OBI, and CVD."""
|
||||
# Mock the subplots creation
|
||||
mock_fig = type('MockFig', (), {})()
|
||||
mock_ax_ohlc = type('MockAx', (), {})()
|
||||
mock_ax_volume = type('MockAx', (), {})()
|
||||
mock_ax_obi = type('MockAx', (), {})()
|
||||
mock_ax_cvd = type('MockAx', (), {})()
|
||||
|
||||
mock_subplots.return_value = (mock_fig, (mock_ax_ohlc, mock_ax_volume, mock_ax_obi, mock_ax_cvd))
|
||||
|
||||
# Create visualizer
|
||||
visualizer = Visualizer(window_seconds=60, max_bars=200)
|
||||
|
||||
# Verify subplots were created correctly
|
||||
mock_subplots.assert_called_once_with(4, 1, figsize=(12, 10), sharex=True)
|
||||
assert visualizer.ax_ohlc == mock_ax_ohlc
|
||||
assert visualizer.ax_volume == mock_ax_volume
|
||||
assert visualizer.ax_obi == mock_ax_obi
|
||||
assert visualizer.ax_cvd == mock_ax_cvd
|
||||
|
||||
|
||||
def test_visualizer_update_from_book_with_empty_book():
|
||||
"""Test that visualizer handles empty book gracefully."""
|
||||
with patch('matplotlib.pyplot.subplots') as mock_subplots:
|
||||
# Mock the subplots creation
|
||||
mock_fig = type('MockFig', (), {'canvas': type('MockCanvas', (), {'draw_idle': lambda: None})()})()
|
||||
mock_axes = [type('MockAx', (), {'clear': lambda: None})() for _ in range(4)]
|
||||
mock_subplots.return_value = (mock_fig, tuple(mock_axes))
|
||||
|
||||
visualizer = Visualizer(window_seconds=60, max_bars=200)
|
||||
|
||||
# Test with empty book
|
||||
book = Book()
|
||||
|
||||
# Should handle gracefully without errors
|
||||
with patch('logging.warning') as mock_warning:
|
||||
visualizer.update_from_book(book)
|
||||
mock_warning.assert_called_once_with("Book has no snapshots to visualize")
|
||||
Reference in New Issue
Block a user