Files
ai/backend.old/src/datasource/adapters/demo.py
2026-03-11 18:47:11 -04:00

354 lines
12 KiB
Python

"""
Demo data source with synthetic data.
Generates realistic-looking OHLCV data plus additional columns for testing.
"""
import asyncio
import logging
import math
import random
import time
from typing import Callable, Dict, List, Optional
from ..base import DataSource
logger = logging.getLogger(__name__)
from ..schema import (
Bar,
ColumnInfo,
DatafeedConfig,
HistoryResult,
Resolution,
SearchResult,
SymbolInfo,
)
class DemoDataSource(DataSource):
"""
Demo data source that generates synthetic time-series data.
Provides:
- Standard OHLCV columns
- Additional demo columns (RSI, sentiment, volume_profile)
- Real-time updates via polling simulation
"""
def __init__(self):
self._subscriptions: Dict[str, asyncio.Task] = {}
self._symbols = {
"DEMO:BTC/USD": {
"name": "Bitcoin",
"type": "crypto",
"base_price": 50000.0,
"volatility": 0.02,
},
"DEMO:ETH/USD": {
"name": "Ethereum",
"type": "crypto",
"base_price": 3000.0,
"volatility": 0.03,
},
"DEMO:SOL/USD": {
"name": "Solana",
"type": "crypto",
"base_price": 100.0,
"volatility": 0.04,
},
}
async def get_config(self) -> DatafeedConfig:
return DatafeedConfig(
name="Demo DataSource",
description="Synthetic data generator for testing. Provides OHLCV plus additional indicator columns.",
supported_resolutions=[
Resolution.M1,
Resolution.M5,
Resolution.M15,
Resolution.H1,
Resolution.D1,
],
supports_search=True,
supports_time=True,
exchanges=["DEMO"],
symbols_types=["crypto"],
)
async def search_symbols(
self,
query: str,
type: Optional[str] = None,
exchange: Optional[str] = None,
limit: int = 30,
) -> List[SearchResult]:
query_lower = query.lower()
results = []
for symbol, info in self._symbols.items():
if query_lower in symbol.lower() or query_lower in info["name"].lower():
if type and info["type"] != type:
continue
results.append(
SearchResult(
symbol=info['name'], # Clean user-facing format (e.g., "Bitcoin")
ticker=symbol, # Keep DEMO:BTC/USD format for routing
full_name=f"{info['name']} (DEMO)",
description=f"Demo {info['name']} pair",
exchange="DEMO",
type=info["type"],
)
)
return results[:limit]
async def resolve_symbol(self, symbol: str) -> SymbolInfo:
if symbol not in self._symbols:
raise ValueError(f"Symbol '{symbol}' not found")
info = self._symbols[symbol]
base, quote = symbol.split(":")[1].split("/")
return SymbolInfo(
symbol=info["name"], # Clean user-facing format (e.g., "Bitcoin")
ticker=symbol, # Keep DEMO:BTC/USD format for routing
name=info["name"],
description=f"Demo {info['name']} spot price with synthetic indicators",
type=info["type"],
exchange="DEMO",
timezone="Etc/UTC",
session="24x7",
supported_resolutions=[Resolution.M1, Resolution.M5, Resolution.M15, Resolution.H1, Resolution.D1],
has_intraday=True,
has_daily=True,
has_weekly_and_monthly=False,
columns=[
ColumnInfo(
name="open",
type="float",
description=f"Opening price in {quote}",
unit=quote,
),
ColumnInfo(
name="high",
type="float",
description=f"Highest price in {quote}",
unit=quote,
),
ColumnInfo(
name="low",
type="float",
description=f"Lowest price in {quote}",
unit=quote,
),
ColumnInfo(
name="close",
type="float",
description=f"Closing price in {quote}",
unit=quote,
),
ColumnInfo(
name="volume",
type="float",
description=f"Trading volume in {base}",
unit=base,
),
ColumnInfo(
name="rsi",
type="float",
description="Relative Strength Index (14-period), range 0-100",
unit=None,
),
ColumnInfo(
name="sentiment",
type="float",
description="Synthetic social sentiment score, range -1.0 to 1.0",
unit=None,
),
ColumnInfo(
name="volume_profile",
type="float",
description="Volume as percentage of 24h average",
unit="%",
),
],
time_column="time",
has_ohlcv=True,
pricescale=100,
minmov=1,
base_currency=base,
quote_currency=quote,
)
async def get_bars(
self,
symbol: str,
resolution: str,
from_time: int,
to_time: int,
countback: Optional[int] = None,
) -> HistoryResult:
if symbol not in self._symbols:
raise ValueError(f"Symbol '{symbol}' not found")
logger.info(
f"DemoDataSource.get_bars: symbol={symbol}, resolution={resolution}, "
f"from_time={from_time}, to_time={to_time}, countback={countback}"
)
info = self._symbols[symbol]
symbol_meta = await self.resolve_symbol(symbol)
# Convert resolution to seconds
resolution_seconds = self._resolution_to_seconds(resolution)
# Generate bars
bars = []
# Align current_time to resolution, but ensure it's >= from_time
current_time = from_time - (from_time % resolution_seconds)
if current_time < from_time:
current_time += resolution_seconds
price = info["base_price"]
bar_count = 0
max_bars = countback if countback else 5000
while current_time <= to_time and bar_count < max_bars:
bar_data = self._generate_bar(current_time, price, info["volatility"], resolution_seconds)
# Only include bars within the requested range
if from_time <= current_time <= to_time:
bars.append(Bar(time=current_time * 1000, data=bar_data)) # Convert to milliseconds
bar_count += 1
price = bar_data["close"] # Next bar starts from previous close
current_time += resolution_seconds
logger.info(
f"DemoDataSource.get_bars: Generated {len(bars)} bars. "
f"First: {bars[0].time if bars else 'N/A'}, Last: {bars[-1].time if bars else 'N/A'}"
)
# Determine if there's more data (for pagination)
next_time = current_time if current_time <= to_time else None
return HistoryResult(
symbol=symbol,
resolution=resolution,
bars=bars,
columns=symbol_meta.columns,
nextTime=next_time,
)
async def subscribe_bars(
self,
symbol: str,
resolution: str,
on_tick: Callable[[dict], None],
) -> str:
if symbol not in self._symbols:
raise ValueError(f"Symbol '{symbol}' not found")
subscription_id = f"{symbol}:{resolution}:{time.time()}"
# Start background task to simulate real-time updates
task = asyncio.create_task(
self._tick_generator(symbol, resolution, on_tick, subscription_id)
)
self._subscriptions[subscription_id] = task
return subscription_id
async def unsubscribe_bars(self, subscription_id: str) -> None:
task = self._subscriptions.pop(subscription_id, None)
if task:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
def _resolution_to_seconds(self, resolution: str) -> int:
"""Convert resolution string to seconds"""
if resolution.endswith("D"):
return int(resolution[:-1]) * 86400
elif resolution.endswith("W"):
return int(resolution[:-1]) * 604800
elif resolution.endswith("M"):
return int(resolution[:-1]) * 2592000 # Approximate month
else:
# Assume minutes
return int(resolution) * 60
def _generate_bar(self, timestamp: int, base_price: float, volatility: float, period_seconds: int) -> dict:
"""Generate a single synthetic OHLCV bar"""
# Random walk for the period
open_price = base_price
# Generate intra-period price movement
num_ticks = max(10, period_seconds // 60) # More ticks for longer periods
prices = [open_price]
for _ in range(num_ticks):
change = random.gauss(0, volatility / math.sqrt(num_ticks))
prices.append(prices[-1] * (1 + change))
close_price = prices[-1]
high_price = max(prices)
low_price = min(prices)
# Volume with some randomness
base_volume = 1000000 * (period_seconds / 60) # Scale with period
volume = base_volume * random.uniform(0.5, 2.0)
# Additional synthetic indicators
rsi = 30 + random.random() * 40 # Biased toward middle range
sentiment = math.sin(timestamp / 3600) * 0.5 + random.gauss(0, 0.2) # Hourly cycle + noise
sentiment = max(-1.0, min(1.0, sentiment))
volume_profile = 100 * random.uniform(0.5, 1.5)
return {
"open": round(open_price, 2),
"high": round(high_price, 2),
"low": round(low_price, 2),
"close": round(close_price, 2),
"volume": round(volume, 2),
"rsi": round(rsi, 2),
"sentiment": round(sentiment, 3),
"volume_profile": round(volume_profile, 2),
}
async def _tick_generator(
self,
symbol: str,
resolution: str,
on_tick: Callable[[dict], None],
subscription_id: str,
):
"""Background task that generates periodic ticks"""
info = self._symbols[symbol]
resolution_seconds = self._resolution_to_seconds(resolution)
# Start from current aligned time
current_time = int(time.time())
current_time = current_time - (current_time % resolution_seconds)
price = info["base_price"]
try:
while True:
# Wait until next bar
await asyncio.sleep(resolution_seconds)
current_time += resolution_seconds
bar_data = self._generate_bar(current_time, price, info["volatility"], resolution_seconds)
price = bar_data["close"]
# Call the tick handler
tick_data = {"time": current_time, **bar_data}
on_tick(tick_data)
except asyncio.CancelledError:
# Subscription cancelled
pass