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:
2025-09-01 11:17:10 +08:00
parent fa6df78c1e
commit 36385af6f3
27 changed files with 1694 additions and 933 deletions

View File

@@ -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)

View File

@@ -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]

View File

@@ -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

View File

@@ -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")