Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions backend/app/market/massive_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ async def _poll_once(self) -> None:
for snap in snapshots:
try:
price = snap.last_trade.price
# Massive timestamps are Unix milliseconds → convert to seconds
timestamp = snap.last_trade.timestamp / 1000.0
# Massive client timestamps are Unix nanoseconds → convert to seconds
timestamp = snap.last_trade.timestamp / 1_000_000_000
self._cache.update(
ticker=snap.ticker,
price=price,
Expand Down
16 changes: 8 additions & 8 deletions backend/tests/market/test_massive.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
from app.market.massive_client import MassiveDataSource


def _make_snapshot(ticker: str, price: float, timestamp_ms: int) -> MagicMock:
def _make_snapshot(ticker: str, price: float, timestamp_ns: int) -> MagicMock:
"""Create a mock Massive snapshot object."""
snap = MagicMock()
snap.ticker = ticker
snap.last_trade = MagicMock()
snap.last_trade.price = price
snap.last_trade.timestamp = timestamp_ms
snap.last_trade.timestamp = timestamp_ns
return snap


Expand All @@ -34,8 +34,8 @@ async def test_poll_updates_cache(self):
source._client = MagicMock() # Satisfy the _poll_once guard

mock_snapshots = [
_make_snapshot("AAPL", 190.50, 1707580800000),
_make_snapshot("GOOGL", 175.25, 1707580800000),
_make_snapshot("AAPL", 190.50, 1707580800000000000),
_make_snapshot("GOOGL", 175.25, 1707580800000000000),
]

with patch.object(source, "_fetch_snapshots", return_value=mock_snapshots):
Expand All @@ -55,7 +55,7 @@ async def test_malformed_snapshot_skipped(self):
source._tickers = ["AAPL", "BAD"]
source._client = MagicMock() # Satisfy the _poll_once guard

good_snap = _make_snapshot("AAPL", 190.50, 1707580800000)
good_snap = _make_snapshot("AAPL", 190.50, 1707580800000000000)
bad_snap = MagicMock()
bad_snap.ticker = "BAD"
bad_snap.last_trade = None # Will cause AttributeError
Expand Down Expand Up @@ -84,7 +84,7 @@ async def test_api_error_does_not_crash(self):
assert cache.get_price("AAPL") is None # No update happened

async def test_timestamp_conversion(self):
"""Test that timestamps are converted from milliseconds to seconds."""
"""Test that timestamps are converted from nanoseconds to seconds."""
cache = PriceCache()
source = MassiveDataSource(
api_key="test-key",
Expand All @@ -94,7 +94,7 @@ async def test_timestamp_conversion(self):
source._tickers = ["AAPL"]
source._client = MagicMock() # Satisfy the _poll_once guard

mock_snapshots = [_make_snapshot("AAPL", 190.50, 1707580800000)]
mock_snapshots = [_make_snapshot("AAPL", 190.50, 1707580800000000000)]

with patch.object(source, "_fetch_snapshots", return_value=mock_snapshots):
await source._poll_once()
Expand Down Expand Up @@ -189,7 +189,7 @@ async def test_start_immediate_poll(self):
cache = PriceCache()
source = MassiveDataSource(api_key="test-key", price_cache=cache, poll_interval=60.0)

mock_snapshots = [_make_snapshot("AAPL", 190.50, 1707580800000)]
mock_snapshots = [_make_snapshot("AAPL", 190.50, 1707580800000000000)]

with patch("app.market.massive_client.RESTClient"):
with patch.object(source, "_fetch_snapshots", return_value=mock_snapshots):
Expand Down
11 changes: 6 additions & 5 deletions planning/MARKET_DATA_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,9 @@ Design decisions:
- **`change` / `change_percent` / `direction` are computed properties, not stored fields.**
They are pure functions of `price` and `previous_price`, so storing them would be a
chance to store them inconsistently. `to_dict()` materialises them for the wire.
- **`timestamp` is Unix seconds (float).** Massive returns milliseconds; the client divides
by 1000 at the boundary so everything inside the layer speaks one unit.
- **`timestamp` is Unix seconds (float).** Massive's `last_trade.timestamp` is in
nanoseconds; the client divides by `1_000_000_000` at the boundary so everything inside
the layer speaks one unit.

Worked example:

Expand Down Expand Up @@ -1101,8 +1102,8 @@ class MassiveDataSource(MarketDataSource):
for snap in snapshots:
try:
price = snap.last_trade.price
# Massive timestamps are Unix milliseconds → convert to seconds
timestamp = snap.last_trade.timestamp / 1000.0
# Massive client timestamps are Unix nanoseconds → convert to seconds
timestamp = snap.last_trade.timestamp / 1_000_000_000
self._cache.update(
ticker=snap.ticker,
price=price,
Expand Down Expand Up @@ -1711,7 +1712,7 @@ if price is not None:
self._cache.update(
ticker=snap.ticker,
price=snap.last_trade.price,
timestamp=snap.last_trade.timestamp / 1000.0,
timestamp=snap.last_trade.timestamp / 1_000_000_000,
baseline=getattr(snap.day, "previous_close", None) or None,
)
```
Expand Down
Loading