backend redesign
This commit is contained in:
0
backend.old/tests/__init__.py
Normal file
0
backend.old/tests/__init__.py
Normal file
199
backend.old/tests/datafeed_client_example.py
Normal file
199
backend.old/tests/datafeed_client_example.py
Normal file
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
Example client demonstrating how to integrate with the datafeed WebSocket API.
|
||||
|
||||
This shows how a TradingView integration or custom charting client would
|
||||
interact with the datafeed.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from typing import Callable, Dict
|
||||
|
||||
import websockets
|
||||
|
||||
|
||||
class DatafeedClient:
|
||||
"""Client for TradingView-compatible datafeed WebSocket API"""
|
||||
|
||||
def __init__(self, uri: str = "ws://localhost:8000/ws/datafeed"):
|
||||
self.uri = uri
|
||||
self.websocket = None
|
||||
self.request_id_counter = 0
|
||||
self.pending_requests: Dict[str, asyncio.Future] = {}
|
||||
self.subscriptions: Dict[str, Callable] = {}
|
||||
self._receive_task = None
|
||||
|
||||
async def connect(self):
|
||||
"""Connect to the datafeed WebSocket"""
|
||||
self.websocket = await websockets.connect(self.uri)
|
||||
self._receive_task = asyncio.create_task(self._receive_loop())
|
||||
print(f"Connected to {self.uri}")
|
||||
|
||||
async def disconnect(self):
|
||||
"""Disconnect from the datafeed"""
|
||||
if self._receive_task:
|
||||
self._receive_task.cancel()
|
||||
if self.websocket:
|
||||
await self.websocket.close()
|
||||
|
||||
async def _receive_loop(self):
|
||||
"""Background task to receive and route messages"""
|
||||
try:
|
||||
async for message in self.websocket:
|
||||
data = json.loads(message)
|
||||
msg_type = data.get("type")
|
||||
|
||||
# Route bar updates to subscription callbacks
|
||||
if msg_type == "bar_update":
|
||||
sub_id = data.get("subscription_id")
|
||||
if sub_id in self.subscriptions:
|
||||
callback = self.subscriptions[sub_id]
|
||||
callback(data["bar"])
|
||||
# Route responses to pending requests
|
||||
elif "request_id" in data:
|
||||
req_id = data["request_id"]
|
||||
if req_id in self.pending_requests:
|
||||
future = self.pending_requests.pop(req_id)
|
||||
future.set_result(data)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Error in receive loop: {e}")
|
||||
|
||||
def _next_request_id(self) -> str:
|
||||
"""Generate next request ID"""
|
||||
self.request_id_counter += 1
|
||||
return f"req_{self.request_id_counter}"
|
||||
|
||||
async def _send_request(self, request: dict) -> dict:
|
||||
"""Send a request and wait for response"""
|
||||
req_id = self._next_request_id()
|
||||
request["request_id"] = req_id
|
||||
|
||||
# Create future for response
|
||||
future = asyncio.Future()
|
||||
self.pending_requests[req_id] = future
|
||||
|
||||
# Send request
|
||||
await self.websocket.send(json.dumps(request))
|
||||
|
||||
# Wait for response
|
||||
return await future
|
||||
|
||||
async def get_config(self) -> dict:
|
||||
"""Get datafeed configuration"""
|
||||
response = await self._send_request({"type": "get_config"})
|
||||
return response["config"]
|
||||
|
||||
async def search_symbols(self, query: str) -> list:
|
||||
"""Search for symbols"""
|
||||
response = await self._send_request({"type": "search_symbols", "query": query})
|
||||
return response["results"]
|
||||
|
||||
async def resolve_symbol(self, symbol: str) -> dict:
|
||||
"""Get symbol metadata"""
|
||||
response = await self._send_request({"type": "resolve_symbol", "symbol": symbol})
|
||||
return response["symbol_info"]
|
||||
|
||||
async def get_bars(
|
||||
self, symbol: str, resolution: str, from_time: int, to_time: int, countback: int = None
|
||||
) -> dict:
|
||||
"""Get historical bars"""
|
||||
request = {
|
||||
"type": "get_bars",
|
||||
"symbol": symbol,
|
||||
"resolution": resolution,
|
||||
"from_time": from_time,
|
||||
"to_time": to_time,
|
||||
}
|
||||
if countback:
|
||||
request["countback"] = countback
|
||||
|
||||
response = await self._send_request(request)
|
||||
return response["history"]
|
||||
|
||||
async def subscribe_bars(
|
||||
self, symbol: str, resolution: str, subscription_id: str, callback: Callable
|
||||
):
|
||||
"""Subscribe to real-time bar updates"""
|
||||
self.subscriptions[subscription_id] = callback
|
||||
|
||||
response = await self._send_request({
|
||||
"type": "subscribe_bars",
|
||||
"symbol": symbol,
|
||||
"resolution": resolution,
|
||||
"subscription_id": subscription_id,
|
||||
})
|
||||
|
||||
if not response.get("success"):
|
||||
raise Exception(f"Subscription failed: {response.get('message')}")
|
||||
|
||||
async def unsubscribe_bars(self, subscription_id: str):
|
||||
"""Unsubscribe from updates"""
|
||||
self.subscriptions.pop(subscription_id, None)
|
||||
await self._send_request({
|
||||
"type": "unsubscribe_bars",
|
||||
"subscription_id": subscription_id,
|
||||
})
|
||||
|
||||
|
||||
async def main():
|
||||
"""Example usage of the DatafeedClient"""
|
||||
client = DatafeedClient()
|
||||
|
||||
try:
|
||||
# Connect
|
||||
await client.connect()
|
||||
|
||||
# Get config
|
||||
config = await client.get_config()
|
||||
print(f"\nDatafeed: {config['name']}")
|
||||
print(f"Supported resolutions: {config['supported_resolutions']}")
|
||||
|
||||
# Search for BTC
|
||||
results = await client.search_symbols("BTC")
|
||||
print(f"\nSearch results for 'BTC': {len(results)} found")
|
||||
for result in results:
|
||||
print(f" - {result['symbol']}: {result['description']}")
|
||||
|
||||
# Get symbol info
|
||||
if results:
|
||||
symbol = results[0]["symbol"]
|
||||
info = await client.resolve_symbol(symbol)
|
||||
print(f"\nSymbol info for {symbol}:")
|
||||
print(f" Name: {info['name']}")
|
||||
print(f" Type: {info['type']}")
|
||||
print(f" Columns: {[c['name'] for c in info['columns']]}")
|
||||
|
||||
# Get historical data
|
||||
now = int(time.time())
|
||||
from_time = now - 3600 # 1 hour ago
|
||||
history = await client.get_bars(symbol, "5", from_time, now, countback=10)
|
||||
print(f"\nHistorical data: {len(history['bars'])} bars")
|
||||
if history["bars"]:
|
||||
print(f" First bar time: {history['bars'][0]['time']}")
|
||||
print(f" Last bar close: {history['bars'][-1]['data']['close']}")
|
||||
|
||||
# Subscribe to real-time updates
|
||||
print(f"\nSubscribing to real-time updates for {symbol}...")
|
||||
|
||||
def on_bar_update(bar):
|
||||
print(f" [UPDATE] Time: {bar['time']}, Close: {bar['close']}")
|
||||
|
||||
await client.subscribe_bars(symbol, "5", "my_subscription", on_bar_update)
|
||||
print(" Waiting for updates (15 seconds)...")
|
||||
await asyncio.sleep(15)
|
||||
|
||||
# Unsubscribe
|
||||
await client.unsubscribe_bars("my_subscription")
|
||||
print(" Unsubscribed")
|
||||
|
||||
finally:
|
||||
await client.disconnect()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== Datafeed Client Example ===\n")
|
||||
print("Make sure the backend server is running on http://localhost:8000")
|
||||
asyncio.run(main())
|
||||
158
backend.old/tests/test_ccxt_datasource.py
Normal file
158
backend.old/tests/test_ccxt_datasource.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""
|
||||
Test script for CCXT DataSource adapter (Free Version).
|
||||
|
||||
This demonstrates how to use the free CCXT adapter (not ccxt.pro) with various
|
||||
exchanges. It uses polling instead of WebSocket for real-time updates.
|
||||
|
||||
CCXT is configured to use Decimal mode internally for precision, but OHLCV data
|
||||
is converted to float for optimal DataFrame/analysis performance.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from decimal import Decimal
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from src.datasource.adapters.ccxt_adapter import CCXTDataSource
|
||||
|
||||
|
||||
async def test_binance_datasource():
|
||||
"""Test Binance exchange data source"""
|
||||
print("=" * 60)
|
||||
print("Testing CCXT DataSource with Binance (Free Version)")
|
||||
print("=" * 60)
|
||||
|
||||
# Initialize Binance datasource with faster polling for testing
|
||||
binance = CCXTDataSource(exchange_id="binance", poll_interval=5)
|
||||
|
||||
try:
|
||||
# Test 1: Get configuration
|
||||
print("\n1. Getting datafeed configuration...")
|
||||
config = await binance.get_config()
|
||||
print(f" Name: {config.name}")
|
||||
print(f" Description: {config.description}")
|
||||
print(f" Supported resolutions: {config.supported_resolutions[:5]}...")
|
||||
print(f" Exchanges: {config.exchanges}")
|
||||
|
||||
# Test 2: Search symbols
|
||||
print("\n2. Searching for BTC symbols...")
|
||||
results = await binance.search_symbols("BTC", limit=5)
|
||||
print(f" Found {len(results)} symbols:")
|
||||
for result in results[:3]:
|
||||
print(f" - {result.symbol}: {result.description}")
|
||||
|
||||
# Test 3: Resolve symbol
|
||||
print("\n3. Resolving symbol metadata for BTC/USDT...")
|
||||
symbol_info = await binance.resolve_symbol("BTC/USDT")
|
||||
print(f" Symbol: {symbol_info.symbol}")
|
||||
print(f" Name: {symbol_info.name}")
|
||||
print(f" Type: {symbol_info.type}")
|
||||
print(f" Exchange: {symbol_info.exchange}")
|
||||
print(f" Columns:")
|
||||
for col in symbol_info.columns:
|
||||
print(f" - {col.name} ({col.type}): {col.description}")
|
||||
|
||||
# Test 4: Get historical bars
|
||||
print("\n4. Fetching historical 1-hour bars for BTC/USDT...")
|
||||
end_time = int(datetime.now().timestamp())
|
||||
start_time = end_time - (24 * 3600) # Last 24 hours
|
||||
|
||||
history = await binance.get_bars(
|
||||
symbol="BTC/USDT",
|
||||
resolution="60", # 1 hour
|
||||
from_time=start_time,
|
||||
to_time=end_time,
|
||||
countback=10,
|
||||
)
|
||||
|
||||
print(f" Retrieved {len(history.bars)} bars")
|
||||
if history.bars:
|
||||
latest = history.bars[-1]
|
||||
print(f" Latest bar:")
|
||||
print(f" Time: {datetime.fromtimestamp(latest.time)}")
|
||||
print(f" Open: {latest.data['open']} (type: {type(latest.data['open']).__name__})")
|
||||
print(f" High: {latest.data['high']} (type: {type(latest.data['high']).__name__})")
|
||||
print(f" Low: {latest.data['low']} (type: {type(latest.data['low']).__name__})")
|
||||
print(f" Close: {latest.data['close']} (type: {type(latest.data['close']).__name__})")
|
||||
print(f" Volume: {latest.data['volume']} (type: {type(latest.data['volume']).__name__})")
|
||||
|
||||
# Verify OHLCV uses float (converted from Decimal for DataFrame performance)
|
||||
assert isinstance(latest.data['close'], float), "OHLCV price should be float type!"
|
||||
assert isinstance(latest.data['volume'], float), "OHLCV volume should be float type!"
|
||||
print(f" ✓ OHLCV data type verified: using native float (CCXT uses Decimal internally)")
|
||||
|
||||
# Test 5: Polling subscription (brief test)
|
||||
print("\n5. Testing polling-based subscription...")
|
||||
print(f" Note: Using free CCXT with {binance._poll_interval}s polling interval")
|
||||
tick_count = [0]
|
||||
|
||||
def on_tick(data):
|
||||
tick_count[0] += 1
|
||||
if tick_count[0] == 1:
|
||||
print(f" Received tick: close={data['close']} (type: {type(data['close']).__name__})")
|
||||
assert isinstance(data['close'], float), "Polled OHLCV data should use float!"
|
||||
|
||||
subscription_id = await binance.subscribe_bars(
|
||||
symbol="BTC/USDT",
|
||||
resolution="1", # 1 minute
|
||||
on_tick=on_tick,
|
||||
)
|
||||
print(f" Subscription ID: {subscription_id}")
|
||||
print(f" Waiting {binance._poll_interval + 2} seconds for first poll...")
|
||||
|
||||
await asyncio.sleep(binance._poll_interval + 2)
|
||||
|
||||
# Unsubscribe
|
||||
await binance.unsubscribe_bars(subscription_id)
|
||||
print(f" ✓ Subscription test complete (received {tick_count[0]} tick(s))")
|
||||
|
||||
finally:
|
||||
await binance.close()
|
||||
print("\n✓ Binance datasource test complete")
|
||||
|
||||
|
||||
async def test_multiple_exchanges():
|
||||
"""Test multiple exchanges"""
|
||||
print("\n" + "=" * 60)
|
||||
print("Testing Multiple Exchanges")
|
||||
print("=" * 60)
|
||||
|
||||
exchanges_to_test = ["binance", "coinbase", "kraken"]
|
||||
|
||||
for exchange_id in exchanges_to_test:
|
||||
print(f"\nTesting {exchange_id}...")
|
||||
try:
|
||||
datasource = CCXTDataSource(exchange_id=exchange_id)
|
||||
config = await datasource.get_config()
|
||||
print(f" ✓ {config.name}")
|
||||
|
||||
# Try to search for ETH
|
||||
results = await datasource.search_symbols("ETH", limit=3)
|
||||
print(f" ✓ Found {len(results)} ETH symbols")
|
||||
|
||||
await datasource.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f" ✗ Error: {e}")
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run all tests"""
|
||||
print("\nCCXT DataSource Adapter Test Suite")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
await test_binance_datasource()
|
||||
await test_multiple_exchanges()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("All tests completed successfully! ✓")
|
||||
print("=" * 60)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ Test failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
130
backend.old/tests/test_datafeed_websocket.py
Normal file
130
backend.old/tests/test_datafeed_websocket.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
Test client for TradingView-compatible datafeed WebSocket API.
|
||||
|
||||
Run this to test the datafeed endpoints:
|
||||
python -m pytest backend/tests/test_datafeed_websocket.py -v -s
|
||||
|
||||
Or run directly:
|
||||
python backend/tests/test_datafeed_websocket.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
|
||||
import websockets
|
||||
|
||||
|
||||
async def test_datafeed():
|
||||
"""Test datafeed WebSocket API"""
|
||||
uri = "ws://localhost:8000/ws/datafeed"
|
||||
|
||||
async with websockets.connect(uri) as websocket:
|
||||
print("✓ Connected to datafeed WebSocket")
|
||||
|
||||
# Test 1: Get config
|
||||
print("\n--- Test 1: Get Config ---")
|
||||
request = {"type": "get_config", "request_id": "req_1"}
|
||||
await websocket.send(json.dumps(request))
|
||||
response = json.loads(await websocket.recv())
|
||||
print(f"Response: {json.dumps(response, indent=2)}")
|
||||
assert response["type"] == "get_config_response"
|
||||
print("✓ Config retrieved")
|
||||
|
||||
# Test 2: Search symbols
|
||||
print("\n--- Test 2: Search Symbols ---")
|
||||
request = {"type": "search_symbols", "request_id": "req_2", "query": "BTC"}
|
||||
await websocket.send(json.dumps(request))
|
||||
response = json.loads(await websocket.recv())
|
||||
print(f"Response: {json.dumps(response, indent=2)}")
|
||||
assert response["type"] == "search_symbols_response"
|
||||
assert len(response["results"]) > 0
|
||||
print(f"✓ Found {len(response['results'])} symbols")
|
||||
|
||||
# Test 3: Resolve symbol
|
||||
print("\n--- Test 3: Resolve Symbol ---")
|
||||
request = {"type": "resolve_symbol", "request_id": "req_3", "symbol": "DEMO:BTC/USD"}
|
||||
await websocket.send(json.dumps(request))
|
||||
response = json.loads(await websocket.recv())
|
||||
print(f"Response: {json.dumps(response, indent=2)}")
|
||||
assert response["type"] == "resolve_symbol_response"
|
||||
symbol_info = response["symbol_info"]
|
||||
print(f"✓ Symbol resolved: {symbol_info['name']}")
|
||||
print(f" Available columns: {[c['name'] for c in symbol_info['columns']]}")
|
||||
|
||||
# Test 4: Get historical bars
|
||||
print("\n--- Test 4: Get Historical Bars ---")
|
||||
now = int(time.time())
|
||||
from_time = now - 3600 # 1 hour ago
|
||||
request = {
|
||||
"type": "get_bars",
|
||||
"request_id": "req_4",
|
||||
"symbol": "DEMO:BTC/USD",
|
||||
"resolution": "5",
|
||||
"from_time": from_time,
|
||||
"to_time": now,
|
||||
"countback": 10,
|
||||
}
|
||||
await websocket.send(json.dumps(request))
|
||||
response = json.loads(await websocket.recv())
|
||||
print(f"Response type: {response['type']}")
|
||||
if response["type"] == "get_bars_response":
|
||||
history = response["history"]
|
||||
print(f"✓ Received {len(history['bars'])} bars")
|
||||
if history["bars"]:
|
||||
print(f" First bar: {history['bars'][0]}")
|
||||
print(f" Last bar: {history['bars'][-1]}")
|
||||
else:
|
||||
print(f"Error: {response}")
|
||||
|
||||
# Test 5: Subscribe to real-time updates
|
||||
print("\n--- Test 5: Subscribe to Real-time Updates ---")
|
||||
request = {
|
||||
"type": "subscribe_bars",
|
||||
"request_id": "req_5",
|
||||
"symbol": "DEMO:BTC/USD",
|
||||
"resolution": "5",
|
||||
"subscription_id": "sub_1",
|
||||
}
|
||||
await websocket.send(json.dumps(request))
|
||||
response = json.loads(await websocket.recv())
|
||||
print(f"Subscription response: {json.dumps(response, indent=2)}")
|
||||
assert response["type"] == "subscribe_bars_response"
|
||||
assert response["success"] is True
|
||||
print("✓ Subscribed successfully")
|
||||
|
||||
# Wait for a few updates
|
||||
print("\n Waiting for real-time updates (10 seconds)...")
|
||||
update_count = 0
|
||||
try:
|
||||
for _ in range(3): # Wait for up to 3 messages
|
||||
response = await asyncio.wait_for(websocket.recv(), timeout=5.0)
|
||||
message = json.loads(response)
|
||||
if message["type"] == "bar_update":
|
||||
update_count += 1
|
||||
print(f" Update {update_count}: {message['bar']}")
|
||||
except asyncio.TimeoutError:
|
||||
print(f" No more updates received (got {update_count} updates)")
|
||||
|
||||
# Test 6: Unsubscribe
|
||||
print("\n--- Test 6: Unsubscribe ---")
|
||||
request = {
|
||||
"type": "unsubscribe_bars",
|
||||
"request_id": "req_6",
|
||||
"subscription_id": "sub_1",
|
||||
}
|
||||
await websocket.send(json.dumps(request))
|
||||
response = json.loads(await websocket.recv())
|
||||
print(f"Unsubscribe response: {json.dumps(response, indent=2)}")
|
||||
assert response["type"] == "unsubscribe_bars_response"
|
||||
assert response["success"] is True
|
||||
print("✓ Unsubscribed successfully")
|
||||
|
||||
print("\n=== All tests passed! ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting datafeed WebSocket tests...")
|
||||
print("Make sure the backend server is running on http://localhost:8000")
|
||||
print()
|
||||
asyncio.run(test_datafeed())
|
||||
54
backend.old/tests/test_websocket.py
Normal file
54
backend.old/tests/test_websocket.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import asyncio
|
||||
import json
|
||||
import websockets
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
async def test_websocket_sync():
|
||||
uri = "ws://localhost:8000/ws"
|
||||
async with websockets.connect(uri) as websocket:
|
||||
# 1. Send hello
|
||||
hello = {
|
||||
"type": "hello",
|
||||
"seqs": {}
|
||||
}
|
||||
await websocket.send(json.dumps(hello))
|
||||
|
||||
# 2. Receive snapshots
|
||||
# Expecting TraderState and StrategyState
|
||||
responses = []
|
||||
for _ in range(2):
|
||||
resp = await websocket.recv()
|
||||
responses.append(json.loads(resp))
|
||||
|
||||
assert any(r["store"] == "TraderState" for r in responses)
|
||||
assert any(r["store"] == "StrategyState" for r in responses)
|
||||
|
||||
# 3. Send a patch for TraderState
|
||||
trader_resp = next(r for r in responses if r["store"] == "TraderState")
|
||||
current_seq = trader_resp["seq"]
|
||||
|
||||
patch_msg = {
|
||||
"type": "patch",
|
||||
"store": "TraderState",
|
||||
"seq": current_seq,
|
||||
"patch": [{"op": "replace", "path": "/status", "value": "busy"}]
|
||||
}
|
||||
await websocket.send(json.dumps(patch_msg))
|
||||
|
||||
# 4. Receive confirmation patch
|
||||
confirm_resp = await websocket.recv()
|
||||
confirm_json = json.loads(confirm_resp)
|
||||
assert confirm_json["type"] == "patch"
|
||||
assert confirm_json["store"] == "TraderState"
|
||||
assert confirm_json["seq"] == current_seq + 1
|
||||
assert confirm_json["patch"][0]["value"] == "busy"
|
||||
|
||||
if __name__ == "__main__":
|
||||
# This script requires the server to be running:
|
||||
# PYTHONPATH=backend/src python3 backend/src/main.py
|
||||
try:
|
||||
asyncio.run(test_websocket_sync())
|
||||
print("Test passed!")
|
||||
except Exception as e:
|
||||
print(f"Test failed: {e}")
|
||||
Reference in New Issue
Block a user