first commit
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
import logging
|
||||
from collections import deque
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 7 days of 1-minute candles
|
||||
MAX_CANDLES = 7 * 24 * 60 # 10080
|
||||
|
||||
TIMEFRAME_MINUTES: Dict[str, int] = {
|
||||
"1m": 1,
|
||||
"1H": 60,
|
||||
"4H": 240,
|
||||
"1D": 1440,
|
||||
"1W": 10080,
|
||||
}
|
||||
|
||||
|
||||
class PriceStore:
|
||||
def __init__(self):
|
||||
self._candles: Dict[str, deque] = {
|
||||
"BTC": deque(maxlen=MAX_CANDLES),
|
||||
"ETH": deque(maxlen=MAX_CANDLES),
|
||||
}
|
||||
|
||||
def update(self, asset: str, candle: dict):
|
||||
"""Add or replace the latest 1m candle for the asset."""
|
||||
asset = asset.upper()
|
||||
if asset not in self._candles:
|
||||
self._candles[asset] = deque(maxlen=MAX_CANDLES)
|
||||
buf = self._candles[asset]
|
||||
# If the last candle has the same timestamp, replace it (in-progress bar)
|
||||
if buf and buf[-1]["time"] == candle["time"]:
|
||||
buf[-1] = candle
|
||||
else:
|
||||
buf.append(candle)
|
||||
|
||||
def get_price_at(self, asset: str, timestamp: datetime) -> Optional[float]:
|
||||
"""Return the close price of the candle closest to timestamp."""
|
||||
asset = asset.upper()
|
||||
buf = self._candles.get(asset)
|
||||
if not buf:
|
||||
return None
|
||||
ts = timestamp.replace(tzinfo=None)
|
||||
target_unix = int(ts.timestamp()) * 1000 # candle times are ms
|
||||
best = None
|
||||
best_diff = float("inf")
|
||||
for candle in buf:
|
||||
diff = abs(candle["time"] - target_unix)
|
||||
if diff < best_diff:
|
||||
best_diff = diff
|
||||
best = candle
|
||||
return best["close"] if best else None
|
||||
|
||||
def get_pct_change(
|
||||
self, asset: str, from_ts: datetime, minutes: int
|
||||
) -> Optional[float]:
|
||||
"""Return % change from from_ts to from_ts + minutes."""
|
||||
asset = asset.upper()
|
||||
buf = self._candles.get(asset)
|
||||
if not buf:
|
||||
return None
|
||||
|
||||
from_unix_ms = int(from_ts.replace(tzinfo=None).timestamp()) * 1000
|
||||
to_unix_ms = from_unix_ms + minutes * 60 * 1000
|
||||
|
||||
# Find candle at from_ts
|
||||
from_candle = self._closest_candle(buf, from_unix_ms)
|
||||
to_candle = self._closest_candle(buf, to_unix_ms)
|
||||
|
||||
if from_candle is None or to_candle is None:
|
||||
return None
|
||||
if from_candle["close"] == 0:
|
||||
return None
|
||||
|
||||
pct = (to_candle["close"] - from_candle["close"]) / from_candle["close"] * 100
|
||||
return round(pct, 4)
|
||||
|
||||
def _closest_candle(self, buf: deque, target_unix_ms: int) -> Optional[dict]:
|
||||
best = None
|
||||
best_diff = float("inf")
|
||||
for candle in buf:
|
||||
diff = abs(candle["time"] - target_unix_ms)
|
||||
if diff < best_diff:
|
||||
best_diff = diff
|
||||
best = candle
|
||||
return best
|
||||
|
||||
def get_candles(self, asset: str, timeframe: str = "1H", limit: int = 200) -> List[dict]:
|
||||
"""Aggregate 1m candles into the requested timeframe and return the last `limit` bars."""
|
||||
asset = asset.upper()
|
||||
buf = self._candles.get(asset)
|
||||
if not buf:
|
||||
return []
|
||||
|
||||
tf_minutes = TIMEFRAME_MINUTES.get(timeframe, 60)
|
||||
|
||||
if tf_minutes == 1:
|
||||
candles = list(buf)[-limit:]
|
||||
return [{**c, "time": c["time"] // 1000} for c in candles]
|
||||
|
||||
# Aggregate
|
||||
aggregated: Dict[int, dict] = {}
|
||||
tf_ms = tf_minutes * 60 * 1000
|
||||
|
||||
for candle in buf:
|
||||
bucket = (candle["time"] // tf_ms) * tf_ms
|
||||
if bucket not in aggregated:
|
||||
aggregated[bucket] = {
|
||||
"time": bucket,
|
||||
"open": candle["open"],
|
||||
"high": candle["high"],
|
||||
"low": candle["low"],
|
||||
"close": candle["close"],
|
||||
"volume": candle["volume"],
|
||||
}
|
||||
else:
|
||||
agg = aggregated[bucket]
|
||||
agg["high"] = max(agg["high"], candle["high"])
|
||||
agg["low"] = min(agg["low"], candle["low"])
|
||||
agg["close"] = candle["close"]
|
||||
agg["volume"] += candle["volume"]
|
||||
|
||||
sorted_candles = sorted(aggregated.values(), key=lambda c: c["time"])
|
||||
result = sorted_candles[-limit:]
|
||||
# Convert ms → seconds for lightweight-charts
|
||||
return [{**c, "time": c["time"] // 1000} for c in result]
|
||||
|
||||
def latest_price(self, asset: str) -> Optional[float]:
|
||||
asset = asset.upper()
|
||||
buf = self._candles.get(asset)
|
||||
if not buf:
|
||||
return None
|
||||
return buf[-1]["close"]
|
||||
|
||||
|
||||
price_store = PriceStore()
|
||||
Reference in New Issue
Block a user