initial commit

This commit is contained in:
sammanme 2025-08-19 17:04:19 +01:00
commit 55475ff7e5
33 changed files with 5673 additions and 0 deletions

32
.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
*README.md
*venv/
__pycache__/
*.pyc
quotes.db
archives/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

5
Quote_Manager_server/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
venv/
__pycache__/
*.pyc
quotes.db
archives/

View File

@ -0,0 +1,31 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from routes import router as quote_router
app = FastAPI(
title="Quote Manager",
description="Quote Manager",
version="1.0.0",
)
origins = [
"http://localhost:5173",
"http://localhost:5173",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# router for quote management
app.include_router(quote_router, prefix="/api/quotes")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000, reload=True)

View File

@ -0,0 +1,31 @@
import logging
import sys
from pathlib import Path
from quote_service import QuoteService
from quote_db import QuoteDatabase
from quote_contracts import IngestAllRequest
logging.basicConfig(level=logging.INFO)
def main():
if len(sys.argv) > 1:
folder_path = Path(sys.argv[1])
else:
folder_path = Path("./archives")
if not folder_path.exists() or not folder_path.is_dir():
logging.error(f"Sorry, folder {folder_path} does not exist or not a directory.")
db = QuoteDatabase("quotes.db")
service = QuoteService(db)
service.ingest_archives_from_folder(folder_path)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,70 @@
import logging
from datetime import datetime
from quote_service import QuoteService
from quote_db import QuoteDatabase
from quote_contracts import (
FetchQuoteRequest,
ListSymbolsRequest,
ListDatesRequest,
ListSessionRequest,
IngestRequest
)
import argparse
from pathlib import Path
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
def to_unix(date_str):
return int(datetime.strptime(date_str, "%Y-%m-%d").timestamp())
def parse_args():
parser = argparse.ArgumentParser(description="Quote Manager Workflow")
parser.add_argument("--broker", required=False, help="Broker name")
parser.add_argument("--symbol", required=False, help="Symbol")
parser.add_argument("--date", required=False, help="Date (YYYY-MM-DD)")
parser.add_argument("--archive", required=False, help="Path to archive zip")
return parser.parse_args()
def main():
args = parse_args()
db = QuoteDatabase("quotes.db")
service = QuoteService(db)
# 1. Ingest archive, redundant since using batch_ingest.py
if args.batch_folder:
service.ingest_archives_from_folder(Path(args.batch_folder))
else:
result = service.ingest_archive(IngestRequest(zip_path=args.archive))
logging.info(f"{result.status} - {result.message}")
# 2. List all brokers
logging.info("All Brokers:")
brokers_response = service.get_all_brokers()
logging.info(f"Brokers: {brokers_response.brokers}")
# 3. List symbols for broker
symbols_response = service.get_symbols(ListSymbolsRequest(broker=args.broker))
logging.info(f"Symbols for broker '{args.broker}': {symbols_response.symbols}")
# 4. List dates for symbol
dates_response = service.get_dates(ListDatesRequest(broker=args.broker, symbol=args.symbol))
logging.info(f"Dates for {args.broker} - {args.symbol}: {dates_response.dates}")
# 5. List sessions for date
sessions_response = service.get_sessions(ListSessionRequest(broker=args.broker, symbol=args.symbol, date=args.date))
logging.info(f"Sessions for {args.broker} - {args.symbol} on {args.date}: {sessions_response.sessions}")
# 6. Fetch quotes
start = to_unix(args.date) * 1000
end = start + 86400 * 1000
quotes_response = service.get_quotes(FetchQuoteRequest(
broker=args.broker,
symbol=args.symbol,
start_time=start,
end_time=end
))
logging.info(f"Quotes fetched: {quotes_response.quotes}")

View File

@ -0,0 +1,68 @@
import os
import csv
import zipfile
import logging
from quote_db import QuoteDatabase
logger = logging.getLogger(__name__)
def extract_metadata_from_filename(filename):
parts = filename.replace(".csv", "").split("_")
broker, symbol, *_session_parts = parts
session_id = "_".join(_session_parts)
return broker, symbol, session_id
def ingest_zip_archive(zip_path: str, db: QuoteDatabase):
archive_name = os.path.basename(zip_path).replace(".zip", "")
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
file_list = zip_ref.infolist()
for file_info in file_list:
if file_info.filename.endswith(".csv"):
logger.info(f"Processing the CSV file: {file_info.filename}")
with zip_ref.open(file_info.filename) as file:
broker, symbol, session_id = extract_metadata_from_filename(file_info.filename)
reader = csv.DictReader((line.decode("utf-8") for line in file))
quotes = []
timestamps = []
for row in reader:
try:
timestamp = int(row["Ts"])
bid = float(row["Bid"])
ask = float(row["Ask"])
quotes.append((timestamp, bid, ask))
timestamps.append(timestamp)
except KeyError as e:
logger.warning(f"Missing column in row: {row} -- Error found says: {e}")
except ValueError as e:
logger.error(f"Conversion error in row: {row} -- Error found says: {e}")
except Exception as e:
logger.error(f"Error processing row: {row} -- Error found says: {e}")
if quotes:
logger.info(f"Parsed {len(quotes)} quotes for session {session_id}")
start_time = min(timestamps)
end_time = max(timestamps)
if db.session_exists(session_id):
logger.info(f"Session {session_id} already exists in database, skipping insertion.")
else:
db.insert_session((session_id, broker, symbol, archive_name, start_time, end_time))
db.insert_quotes_bulk(session_id, quotes)
else:
logger.warning(f"No quotes parsed from the archive file: {file_info.filename}")
db.conn.commit()
logger.info(f"Ingestion completed for archive file: {archive_name}")

View File

@ -0,0 +1,79 @@
from pydantic import BaseModel, Field
from typing import List, Optional, Dict
class FetchData(BaseModel):
bid_price: Optional[float] = Field(default=None)
ask_price: Optional[float] = Field(default=None)
timestamp: str
broker: str
symbol: str
spread: Optional[float] = Field(default=None)
midline: Optional[float] = Field(default=None)
class FetchDataResponse(BaseModel):
data: List[FetchData]
class FetchQuoteRequest(BaseModel):
broker: str
symbol: Optional[str] = None
date: Optional[str] = None
start_time: Optional[int] = None
end_time: Optional[int] = None
class QuoteResponse(BaseModel):
timestamp: int
bid: float
ask: float
session_id: str
class BrokersSymbolsResponse(BaseModel):
brokers: Dict[str, List[str]]
class FetchQuoteResponse(BaseModel):
quotes: List[QuoteResponse]
class FetchBrokersResponse(BaseModel):
brokers: List[str]
class ListSymbolsRequest(BaseModel):
broker: str
class ListSymbolsResponse(BaseModel):
symbols: List[str]
class ListDatesRequest(BaseModel):
broker: str
symbol: str
class ListDatesResponse(BaseModel):
dates: List[str]
class ListSessionRequest(BaseModel):
broker: str
symbol: str
date: str
class ListSessionResponse(BaseModel):
sessions: List[str]
class IngestRequest(BaseModel):
zip_path: str
class IngestResponse(BaseModel):
status: str
message: str
class IngestAllRequest(BaseModel):
folder_path: str
class IngestAllResponse(BaseModel):
status: str
results: List[IngestResponse]

View File

@ -0,0 +1,267 @@
import sqlite3
from typing import List, Tuple, Optional
from datetime import datetime, timezone
import logging
import pandas as pd
logger = logging.getLogger(__name__)
class QuoteDatabase:
db_path = "quotes.db"
def __init__(self, db_path=db_path):
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self.conn.execute("PRAGMA foreign_keys = ON")
self._create_tables()
def _create_tables(self):
if self.conn:
try:
self.conn.execute("""
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
broker TEXT NOT NULL,
symbol TEXT NOT NULL,
archive_name TEXT NOT NULL,
start_time INTEGER NOT NULL,
end_time INTEGER NOT NULL
);
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS quotes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
timestamp INTEGER NOT NULL,
bid REAL NOT NULL,
ask REAL NOT NULL,
FOREIGN KEY(session_id) REFERENCES sessions(session_id),
UNIQUE(session_id, timestamp) ON CONFLICT REPLACE
)
""")
self.conn.commit()
logger.info(f"Table has been created succesfully")
except sqlite3.Error as e:
logger.error(f"Error while creating the table")
else:
logger.info(f"Database connection did not activate, check connection")
def session_exists(self, session_id: str) -> bool:
query = "SELECT 1 FROM sessions WHERE session_id = ? LIMIT 1"
cursor = self.conn.execute(query, (session_id,))
result = cursor.fetchone()
if result is not None:
logging.error(f"This session has existed somewhere on the database before, kindly confirm.")
return result
else:
logging.info(f"Session is not on database before, processing.")
def insert_session(self, session_data: Tuple[str, str, str, str, int, int]):
logger.info(f"Inserting session: {session_data[0]}")
"""Insert a session: (session_id, broker, symbol, archive_name, start_time, end_time)"""
self.conn.execute("""
INSERT INTO sessions (session_id, broker, symbol, archive_name, start_time, end_time)
VALUES (?, ?, ?, ?, ?, ?)
""", session_data)
self.conn.commit()
logger.info(f"Inserted session: {session_data[0]}, time range: {session_data[4]} to {session_data[5]}")
def insert_quotes_bulk(self, session_id: str, quotes: List[Tuple[int, float, float]]):
if not quotes:
logger.warning(f"Skipped empty quote list for session {session_id}")
return
query = """
INSERT INTO quotes (session_id, timestamp, bid, ask)
VALUES (?, ?, ?, ?)
ON CONFLICT(session_id, timestamp)
DO UPDATE SET
bid = excluded.bid,
ask = excluded.ask
"""
try:
self.conn.executemany(query, [(session_id, *q) for q in quotes])
self.conn.commit()
logger.info(f"Quotes inserted successfully for session {session_id} ({len(quotes)} quotes)")
except Exception as e:
logger.error(f"Error inserting quotes for session {session_id}: {e}", exc_info=True)
def fetch_quotes(
self,
broker: Optional[str] = None,
symbol: Optional[str] = None,
start_time: Optional[int] = None,
end_time: Optional[int] = None
) -> List[Tuple[str, str, float, float]]:
self.conn.execute("""
SELECT q.session_id, q.timestamp, q.bid, q.ask
FROM quotes q
JOIN sessions s ON q.session_id = s.session_id
WHERE 1=1
""")
params = []
if broker is not None:
query += " AND s.broker = ?"
params.append(broker)
if symbol is not None:
query += " AND s.symbol = ?"
params.append(symbol)
if start_time is not None:
query += " AND q.timestamp >= ?"
params.append(start_time)
if end_time is not None:
query += " AND q.timestamp <= ?"
params.append(end_time)
cursor = self.conn.execute(query, params)
results = cursor.fetchall()
def to_iso(ts: int) -> str:
if ts > 1e12:
ts = ts / 1000
return datetime.fromtimestamp(ts / 1000, tz=timezone.utc).isoformat().replace("+00:00", "Z")
result = []
for session_id, timestamp, bid, ask in results:
result.append((session_id, to_iso(timestamp), bid, ask))
return result
def get_all_brokers(self) -> List[str]:
cursor = self.conn.execute("SELECT DISTINCT broker FROM sessions")
result = []
for row in cursor.fetchall():
result.append(row[0])
return result
def get_symbols_by_broker(self, broker: str) -> List[str]:
cursor = self.conn.execute("""
SELECT DISTINCT symbol FROM sessions
WHERE broker = ?
""", (broker,))
result = []
for row in cursor.fetchall():
result.append(row[0])
return result
def get_brokers_and_symbols(self):
cursor = self.conn.execute("SELECT DISTINCT broker FROM sessions")
brokers_list = [row[0] for row in cursor.fetchall()]
result = {}
for broker in brokers_list:
result[broker] = self.get_symbols_by_broker(broker)
return result
def get_dates_by_broker_symbol(self, broker: str, symbol: str) -> List[str]:
cursor = self.conn.execute("""
SELECT DISTINCT DATE(q.timestamp / 1000, 'unixepoch')
FROM quotes q
JOIN sessions s ON q.session_id = s.session_id
WHERE s.broker = ? AND s.symbol = ?
ORDER BY 1
""", (broker, symbol))
result = []
for row in cursor.fetchall():
result.append(row[0])
return result
def get_sessions_by_date(self, broker: str, symbol: str, date: str) -> List[str]:
cursor = self.conn.execute("""
SELECT DISTINCT s.session_id
FROM sessions s
JOIN quotes q ON s.session_id = q.session_id
WHERE s.broker = ? AND s.symbol = ? AND DATE(q.timestamp / 1000, 'unixepoch') = ?
""", (broker, symbol, date))
result = []
for row in cursor.fetchall():
result.append(row[0])
return result
def get_quotes_by_session(self, session_id: str) -> List[Tuple[str, float, float]]:
cursor = self.conn.execute("""
SELECT datetime(timestamp, 'unixepoch') as ts, bid, ask
FROM quotes
WHERE session_id = ?
ORDER BY timestamp
""", (session_id,))
return cursor.fetchall()
def get_sessions_by_date_range(self, broker: str, symbol: str, date: str) -> List[str]:
cursor = self.conn.execute("""
SELECT session_id
FROM sessions
WHERE broker = ? AND symbol = ? AND DATE(start_time, 'unixepoch') <= ? AND DATE(end_time, 'unixepoch') >= ?
ORDER BY start_time
""", (broker, symbol, date, date))
result = []
for row in cursor.fetchall():
result.append(row[0])
return result
def get_data(self, broker_a, symbol_a, broker_b, symbol_b, limit=1000, time_range_hours='all'):
try:
available_brokers = pd.read_sql_query("SELECT DISTINCT broker FROM sessions", self.conn)['broker'].tolist()
available_symbols = pd.read_sql_query("SELECT DISTINCT symbol FROM sessions", self.conn)['symbol'].tolist()
if broker_a not in available_brokers or broker_b not in available_brokers:
print(f"Broker not found: broker_a={broker_a}, broker_b={broker_b}, available={available_brokers}")
return pd.DataFrame()
if symbol_a not in available_symbols or symbol_b not in available_symbols:
print(f"Symbol not found: symbol_a={symbol_a}, symbol_b={symbol_b}, available={available_symbols}")
return pd.DataFrame()
query = """
SELECT q.session_id, q.bid, q.ask, q.timestamp, s.broker, s.symbol
FROM quotes q
JOIN sessions s ON q.session_id = s.session_id
WHERE (
(s.broker = ? AND s.symbol = ?) OR
(s.broker = ? AND s.symbol = ?)
)
"""
params = [broker_a, symbol_a, broker_b, symbol_b]
if time_range_hours != 'all':
current_time_ms = int(datetime.now().timestamp() * 1000)
time_range_ms = int(time_range_hours) * 3600 * 1000
query += " AND q.timestamp >= ?"
params.append(current_time_ms - time_range_ms)
query += " ORDER BY q.timestamp DESC LIMIT ?"
params.append(limit)
df = pd.read_sql_query(query, self.conn, params=tuple(params))
if df.empty:
print(f"No data returned for query: broker_a={broker_a}, symbol_a={symbol_a}, "
f"broker_b={broker_b}, symbol_b={symbol_b}, time_range={time_range_hours}")
return pd.DataFrame()
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms', errors='coerce')
return df
except Exception as e:
print(f"Error in get_data: {e}")
return pd.DataFrame()
def close(self):
if self.conn:
self.conn.close()
def __del__(self):
self.close()

View File

@ -0,0 +1,223 @@
from decimal import Decimal
import pandas as pd
import logging
from typing import Union
from sqlalchemy import Float
from ingest import ingest_zip_archive
from quote_db import QuoteDatabase
from quote_contracts import (
FetchQuoteRequest,
QuoteResponse,
FetchQuoteResponse,
FetchBrokersResponse,
ListSymbolsRequest,
ListSymbolsResponse,
ListDatesRequest,
ListDatesResponse,
ListSessionRequest,
ListSessionResponse,
IngestRequest,
IngestResponse,
FetchData,
FetchDataResponse,
BrokersSymbolsResponse,
IngestAllRequest,
IngestAllResponse
)
from pathlib import Path
logger = logging.getLogger(__name__)
class QuoteService:
def __init__(self, db: QuoteDatabase):
self.db = db
def ingest_archives_from_folder(self, folder_path: Union[Path, IngestAllRequest]) -> IngestAllResponse:
results = []
if isinstance(folder_path, IngestAllRequest):
folder_path = Path(folder_path.folder_path)
else:
folder_path = folder_path
if folder_path.exists():
if folder_path.is_dir():
for zip_file in folder_path.rglob("*.zip"):
ingest_request = IngestRequest(zip_path=str(zip_file))
response = self.ingest_archive(ingest_request)
results.append(response)
return IngestAllResponse(
status = "All done cheers.",
results = results
)
else:
return IngestAllResponse(
status = "Oops!, this is not a directory",
results = [
IngestResponse(
status= "There is an error",
message= "Select a folder please!"
)
]
)
else:
return IngestAllResponse(
status = "Oops!, this path does not exist",
results = [
IngestResponse(
status= "There is an error",
message= "Invalid path to directory selected"
)
]
)
def ingest_archive(self, request: IngestRequest) -> IngestResponse:
"""Ingest a ZIP archive into the database."""
zip_path_str = request.zip_path
zip_path = Path(zip_path_str)
if not zip_path.exists():
logger.warning(f"ZIP archive not found: {zip_path}")
return IngestResponse(status="failed", message="ZIP archive not found.")
try:
ingest_zip_archive(zip_path, db=self.db)
return IngestResponse(
status="success",
message="Archive ingested successfully."
)
except Exception as e:
logger.error(f"Ingestion error: {e}", exc_info=True)
return IngestResponse(
status="failed",
message=f"Ingestion error: {e}"
)
def get_all_brokers(self) -> FetchBrokersResponse:
"""Return all brokers in the database."""
brokers = self.db.get_all_brokers()
return FetchBrokersResponse(brokers=brokers)
def get_brokers_and_symbols(self) -> BrokersSymbolsResponse:
"""Return all brokers and their symbols in mapping form."""
mapping = self.db.get_brokers_and_symbols()
return BrokersSymbolsResponse(brokers=mapping)
def get_symbols(self, request: ListSymbolsRequest) -> ListSymbolsResponse:
"""Return all symbols for a broker."""
symbols = self.db.get_symbols_by_broker(request.broker)
return ListSymbolsResponse(symbols=symbols)
def get_dates(self, request: ListDatesRequest) -> ListDatesResponse:
"""Return all dates for a broker and symbol."""
dates = self.db.get_dates_by_broker_symbol(request.broker, request.symbol)
return ListDatesResponse(dates=dates)
def get_sessions(self, request: ListSessionRequest) -> ListSessionResponse:
"""Return all sessions for a broker, symbol, and date."""
sessions = self.db.get_sessions_by_date(
request.broker, request.symbol, request.date
)
return ListSessionResponse(sessions=sessions)
def get_quotes(self, request: FetchQuoteRequest) -> FetchQuoteResponse:
"""Return all quotes for a broker, symbol, and time range."""
quotes = self.db.fetch_quotes(
broker=request.broker,
symbol=request.symbol,
start_time=request.start_time,
end_time=request.end_time
)
return FetchQuoteResponse(
quotes=[
QuoteResponse(
session_id=row[0],
timestamp=row[1],
bid=row[2],
ask=row[3]
)
for row in quotes
]
)
def get_data(self, broker_a, symbol_a, broker_b, symbol_b, limit=1000, time_range_hours='all'):
df = self.db.get_data(broker_a, symbol_a, broker_b, symbol_b, limit, time_range_hours)
if df.empty:
print(f"No data after initial fetch: broker_a={broker_a}, symbol_a={symbol_a}, "
f"broker_b={broker_b}, symbol_b={symbol_b}")
return []
df_a = df[(df['broker'] == broker_a) & (df['symbol'] == symbol_a)].copy()
df_b = df[(df['broker'] == broker_b) & (df['symbol'] == symbol_b)].copy()
result = []
df_a['spread'] = df_a['ask'] - df_a['bid']
df_b['spread'] = df_b['ask'] - df_b['bid']
df_a['midline'] = (df_a['ask'] + df_a['bid']) / 2
df_b['midline'] = (df_b['ask'] + df_b['bid']) / 2
if not df_a.empty:
df_a = df_a.dropna(subset=['timestamp'])
if df_a.empty:
print(f"No valid data for {broker_a}/{symbol_a} after dropping invalid timestamps")
else:
result_a = [
FetchData(
# session_id=row['session_id'],
bid_price=(row['bid']) if pd.notna(row['bid']) else None,
ask_price=(row['ask']) if pd.notna(row['ask']) else None,
timestamp=str(row['timestamp']),
broker=broker_a,
symbol=symbol_a,
spread=row['spread'] if pd.notna(row['spread']) else None,
midline=row['midline'] if pd.notna(row['midline']) else None
)
for _, row in df_a.iterrows()
]
result.extend(result_a)
if not df_b.empty:
df_b = df_b.dropna(subset=['timestamp'])
if df_b.empty:
print(f"No valid data for {broker_b}/{symbol_b} after dropping invalid timestamps")
else:
result_b = [
FetchData(
# session_id=row['session_id'],
bid_price=(row['bid']) if pd.notna(row['bid']) else None,
ask_price=(row['ask']) if pd.notna(row['ask']) else None,
timestamp=str(row['timestamp']),
broker=broker_b,
symbol=symbol_b,
spread=row['spread'] if pd.notna(row['spread']) else None,
midline=row['midline'] if pd.notna(row['midline']) else None
)
for _, row in df_b.iterrows()
]
result.extend(result_b)
if not result:
print(f"No valid data after processing: broker_a={broker_a}, symbol_a={symbol_a}, "
f"broker_b={broker_b}, symbol_b={symbol_b}")
return []
return result[:limit]

Binary file not shown.

View File

@ -0,0 +1,23 @@
annotated-types==0.7.0
anyio==4.10.0
click==8.2.1
fastapi==0.116.1
greenlet==3.2.4
h11==0.16.0
idna==3.10
logging==0.4.9.6
numpy==2.3.2
pandas==2.3.1
pathlib==1.0.1
pydantic==2.11.7
pydantic_core==2.33.2
python-dateutil==2.9.0.post0
pytz==2025.2
six==1.17.0
sniffio==1.3.1
SQLAlchemy==2.0.43
starlette==0.47.2
typing-inspection==0.4.1
typing_extensions==4.14.1
tzdata==2025.2
uvicorn==0.35.0

View File

@ -0,0 +1,23 @@
import os
import logging
from quote_db import QuoteDatabase
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
DB_PATH = "quotes.db"
try:
if os.path.exists(DB_PATH):
os.remove(DB_PATH)
logging.info(f"Deleted old database: {DB_PATH}")
my_class = QuoteDatabase()
my_class._create_tables()
logging.info(f"Fresh database has been created: {DB_PATH}")
except Exception as e:
logging.error(f"Error resetting database: {e}", exc_info=True)

View File

@ -0,0 +1,74 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from pathlib import Path
import logging
from quote_db import QuoteDatabase
from quote_service import QuoteService
from quote_contracts import (
FetchQuoteRequest, FetchQuoteResponse,
ListSymbolsRequest, ListSymbolsResponse,
ListDatesRequest, ListDatesResponse,
ListSessionRequest, ListSessionResponse,
IngestRequest, IngestResponse,
FetchBrokersResponse, FetchDataResponse,
BrokersSymbolsResponse, IngestAllRequest, IngestAllResponse
)
router = APIRouter()
def get_db():
db = QuoteDatabase("quotes.db")
try:
yield db
finally:
db.conn.close()
def get_service(db: QuoteDatabase = Depends(get_db)):
return QuoteService(db=db)
@router.post("/ingestall", response_model = IngestAllResponse)
def ingest_archives(request: IngestAllRequest, service: QuoteService = Depends(get_service)):
return service.ingest_archives_from_folder(request)
@router.post("/ingest", response_model=IngestResponse)
def ingest_quotes(request: IngestRequest, service: QuoteService = Depends(get_service)):
return service.ingest_archive(request)
@router.get("/api/data", response_model=FetchDataResponse)
async def get_data(
broker_a: str = Query(...),
symbol_a: str = Query(...),
broker_b: str = Query(...),
symbol_b: str = Query(...),
time_range_hours: str = Query('all', description="Time range: 'all' or hours (1, 6, 24)"),
limit: int = Query(1000, description="Maximum number of records"),
service: QuoteService = Depends(get_service)
):
data = service.get_data(broker_a, symbol_a, broker_b, symbol_b, limit, time_range_hours)
return {"data": data}
@router.get("/brokers", response_model=FetchBrokersResponse)
def get_all_brokers(service: QuoteService = Depends(get_service)):
return service.get_all_brokers()
@router.post("/symbols", response_model=ListSymbolsResponse)
def get_symbols(request: ListSymbolsRequest, service: QuoteService = Depends(get_service)):
return service.get_symbols(request)
@router.post("/dates", response_model=ListDatesResponse)
def get_dates(request: ListDatesRequest, service: QuoteService = Depends(get_service)):
return service.get_dates(request)
@router.post("/sessions", response_model=ListSessionResponse)
def get_sessions(request: ListSessionRequest, service: QuoteService = Depends(get_service)):
return service.get_sessions(request)
@router.post("/quotes", response_model=FetchQuoteResponse)
def get_quotes(request: FetchQuoteRequest, service: QuoteService = Depends(get_service)):
return service.get_quotes(request)
@router.get("/brokers&symbols", response_model=BrokersSymbolsResponse)
def get_brokers_and_symbols(service: QuoteService = Depends(get_service)):
return service.get_brokers_and_symbols()

24
quote_plotter_client/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config'
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Quote Plotter</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

3797
quote_plotter_client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
{
"name": "quote_plotter_vite",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.12",
"@tanstack/react-query": "^5.85.3",
"echarts-for-react": "^3.0.2",
"query-string": "^9.2.2",
"react": "^19.1.1",
"react-dom": "^19.1.1"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.0",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"tailwindcss": "^4.1.12",
"typescript": "~5.8.3",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2"
}
}

View File

@ -0,0 +1,4 @@
@import "tailwindcss";

View File

@ -0,0 +1,23 @@
import React from 'react';
import { QueryContextProvider } from './context/querycontext';
import DropDownContexts from './components/dropDownComponents';
// import TableComponents from './components/tableComponents';
import ChartComponents from './components/chartComponents';
import './App.css';
const App: React.FC = () => {
return (
<div>
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-800 mb-1">Quote Plotter</h1>
<QueryContextProvider>
{<DropDownContexts />}
{<ChartComponents />}
{/* {<TableComponents />} */}
</QueryContextProvider>
</div>
)
};
export default App;

View File

@ -0,0 +1,110 @@
import { useQuery } from "@tanstack/react-query";
import qs from 'query-string';
import { type RawBrokerSymbol, type QueryOptions, type QuoteData, type QuoteRecord } from "./dataTypes";
export const useBrokerSymbols = () => {
return useQuery<RawBrokerSymbol, Error>({
queryKey: ['brokers'],
queryFn: async(): Promise<RawBrokerSymbol> => {
const response = await fetch('http://localhost:8000/api/quotes/brokers&symbols', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
// console.error('broker and symbol data fetch error:', errorData);
throw new Error('Unable to fetch broker and symbols');
}
return response.json();
},
refetchInterval: 10 * 60 * 1000,
refetchOnReconnect: "always",
staleTime: 1000 * 30,
});
};
function quoteDataLink(params: QueryOptions): string {
const query = qs.stringify({
broker_a: params.brokerA,
symbol_a: params.symbolA,
broker_b: params.brokerB,
symbol_b: params.symbolB,
},
{ skipEmptyString: true, skipNull: true });
return `http://localhost:8000/api/quotes/api/data?${query}`;
}
export const useQuoteData = (params: QueryOptions) => {
return useQuery<QuoteData, Error>({
queryKey: ['quoteData', params],
queryFn: async () => {
const url = quoteDataLink(params);
// console.log('Fetching quote data from:', url);
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
// console.error('Quote data fetch error:', errorData);
throw new Error('Unable to fetch quote data');
}
const quoteData = await response.json();
// console.log('Fetched quote data:', quoteData);
const records = quoteData.data || [];
// console.log('Records:', records);
const recordsA = records.filter(
(record: QuoteRecord) => record.broker === params.brokerA && record.symbol
=== params.symbolA
);
const recordsB = records.filter(
(record: QuoteRecord) => record.broker === params.brokerB && record.symbol
=== params.symbolB
);
// console.log('recordsA:', recordsA, 'recordsB:', recordsB);
if (!recordsA || !recordsB) {
throw new Error('No matching records found for the selected brokers and symbols');
}
const timestamps = [...new Set([...recordsA, ...recordsB].map(record => record.timestamp))
].sort();
const quoteDataResult: QuoteData = {
brokerA: params.brokerA,
brokerB: params.brokerB,
symbolA: params.symbolA,
symbolB: params.symbolB,
records: timestamps.map(timestamp => {
const recordA = recordsA.find((record: QuoteRecord) =>
record.timestamp === timestamp) || {};
const recordB = recordsB.find((record: QuoteRecord) =>
record.timestamp === timestamp) || {};
return {
askA: recordA.ask_price ?? null,
bidA: recordA.bid_price ?? null,
askB: recordB.ask_price ?? null,
bidB: recordB.bid_price ?? null,
spreadA: recordA.spread ?? null,
spreadB: recordB.spread ?? null,
midlineA: recordA.midline ?? null,
midlineB: recordB.midline ?? null,
timestamp,
};
})
};
// console.log('Processed quote data:', quoteDataResult);
return quoteDataResult;
},
enabled: !!params.brokerA && !!params.symbolA && !!params.brokerB && !!params.symbolB,
staleTime: 1000 * 60 * 5,
refetchOnReconnect: "always",
});
};

View File

@ -0,0 +1,50 @@
export interface RawBrokerSymbol {
brokers: {
[brokerName: string]: string[];
};
}
export interface QueryOptions {
brokerA: string;
symbolA: string;
brokerB: string;
symbolB: string;
}
export interface QuoteRecord {
bid_price: number;
ask_price: number;
spread: number;
midline: number;
timestamp: string;
broker: string;
symbol: string;
}
export interface QuoteData {
brokerA: string;
symbolA: string;
brokerB: string;
symbolB: string;
records: {
askA: number | null;
askB: number | null;
bidA: number | null;
bidB: number | null;
spreadA: number | null;
spreadB: number | null;
midlineA: number | null;
midlineB: number | null;
timestamp: string;
}[];
}
export interface QueryContextType {
queryOptions: QueryOptions;
brokers: { [key: string]: string[] };
handleBrokerChange: (newOptions: Partial<QueryOptions>) => void;
setQueryOptions: React.Dispatch<React.SetStateAction<QueryOptions>>;
isLoadingBrokers: boolean;
isErrorBrokers: boolean;
brokersError: Error| null;
}

View File

@ -0,0 +1,326 @@
import React, {useRef, useState} from "react";
import { useQuoteData } from "../QueryOptions/apiQueryOptions";
import { useQueryContext } from "../context/querycontext";
import * as echarts from 'echarts';
import ReactECharts from 'echarts-for-react';
const ChartComponents: React.FC = () => {
const chartRef = useRef<ReactECharts>(null);
const { queryOptions, isLoadingBrokers, isErrorBrokers } =
useQueryContext();
const { data: quoteData, isLoading: isLoadingQuotes, isError: isErrorQuotes } =
useQuoteData(queryOptions);
const [showMidline, setShowMidline] = useState(false);
// console.log('quote data before render:', quoteData,
// 'isLoadingQuotes:', isLoadingQuotes,
// 'quotesError:', isErrorQuotes);
if (isLoadingBrokers) return <div>Loading brokers...</div>;
if (isErrorBrokers) return <div>Error loading brokers</div>;
if (isErrorQuotes) return <div>Error loading quote data</div>;
if (!quoteData?.records?.length) return <div>No quote data available</div>;
const datasetSource = quoteData.records.filter(record => record.askA !== null || record.midlineA
|| record.bidA !== null || record.askB !== null || record.bidB !== null || record.midlineB)
.map(record => ({
timestamp: record.timestamp,
askA: record.askA,
askB: record.askB,
bidA: record.bidA,
bidB: record.bidB,
midlineA: record.midlineA,
midlineB: record.midlineB,
}));
// console.log('datasetSource:', datasetSource);
const valuesA = datasetSource
.flatMap(record => [record.askA, record.bidA, record.midlineA])
.filter((value): value is number => value !== null);
const valuesB = datasetSource
.flatMap(record => [record.askB, record.bidB, record.midlineB])
.filter((value): value is number => value !== null);
const buffer = 0.00001;
// console.log('ValueA:', valuesA);
// console.log('ValueB:', valuesB);
// console.log('askA values:', datasetSource.map(r => r.askA).filter(v => v !== null));
// console.log('askB values:', datasetSource.map(r => r.askB).filter(v => v !== null));
// console.log('bidA values:', datasetSource.map(r => r.bidA).filter(v => v !== null));
// console.log('bidB values:', datasetSource.map(r => r.bidB).filter(v => v !== null));
const minValueA = valuesA.length ? Math.min(...valuesA) - buffer : 0;
const maxValueA = valuesA.length ? Math.max(...valuesA) + buffer : 100;
const minValueB = valuesB.length ? Math.min(...valuesB) - buffer : 0;
const maxValueB = valuesB.length ? Math.max(...valuesB) + buffer : 100;
// console.log('minValueA:', minValueA);
// console.log('maxValueA:', maxValueA);
// console.log('minValueB:', minValueB);
// console.log('maxValueB:', maxValueB);
//
const option = {
dataset: {
source: datasetSource,
dimensions: ['timestamp', 'askA', 'askB', 'bidA', 'bidB', 'midlineA', 'midlineB'],
},
tooltip: {
trigger: 'axis',
formatter: (params: any) => {
const date = new Date(params[0].value[0]);
return (
`Time: ${date.toLocaleTimeString()}<br>` +
params
.map((p: any) => `${p.seriesName}: ${p.value[1] != null ? p.value[1].toFixed(5) : 'N/A'}`)
.join('<br>')
);
},
},
legend: {
data: [
`Ask A (${quoteData.brokerA}/${quoteData.symbolA})`,
`Bid A (${quoteData.brokerA}/${quoteData.symbolA})`,
`Midline A (${quoteData.brokerA}/${quoteData.symbolA})`,
`Ask B (${quoteData.brokerB}/${quoteData.symbolB})`,
`Bid B (${quoteData.brokerB}/${quoteData.symbolB})`,
`Midline B (${quoteData.brokerB}/${quoteData.symbolB})`,
],
selected: {
[`Ask A (${quoteData.brokerA}/${quoteData.symbolA})`]: !showMidline,
[`Ask B (${quoteData.brokerB}/${quoteData.symbolB})`]: !showMidline,
[`Bid A (${quoteData.brokerA}/${quoteData.symbolA})`]: !showMidline,
[`Bid B (${quoteData.brokerB}/${quoteData.symbolB})`]: !showMidline,
[`Midline A (${quoteData.brokerA}/${quoteData.symbolA})`]: showMidline,
[`Midline B (${quoteData.brokerB}/${quoteData.symbolB})`]: showMidline,
},
},
grid: {
left: '5%',
right: '5%',
bottom: '15%',
containLabel: true,
},
toolbox: {
feature: {
saveAsImage: {},
},
},
dataZoom: [
{
type: 'slider',
xAxisIndex: 0,
start: 80,
end: 100,
labelFormatter: (value: number) => {
const date = new Date(value);
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`;
},
},
{
type: 'inside',
xAxisIndex: 0,
start: 80,
end: 100,
},
{
type: 'slider',
yAxisIndex: 0,
start: 0,
end: 100,
labelFormatter: (value: number) => value.toFixed(5),
},
{
type: 'inside',
yAxisIndex: 0,
start: 0,
end: 100,
},
{
type: 'slider',
yAxisIndex: 1,
start: 0,
end: 100,
labelFormatter: (value: number) => value.toFixed(5),
},
{
type: 'inside',
yAxisIndex: 1,
start: 0,
end: 100,
},
],
xAxis: {
type: 'time',
name: 'Timestamp',
axisLabel: {
formatter: (value: number) => {
const date = new Date(value);
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`;
},
},
},
yAxis: [
{
type: 'value',
name: `${quoteData.brokerA}/${quoteData.symbolA}`,
min: minValueA,
max: maxValueA,
axisLabel: {
formatter: (value: number) => value.toFixed(5),
},
// alignTicks:true,
scale: true,
splitNumber: 10,
},
{
type: 'value',
name: `${quoteData.brokerB}/${quoteData.symbolB}`,
min: minValueB,
max: maxValueB,
position: 'right',
axisLabel: {
formatter: (value: number) => value.toFixed(5),
},
// alignTicks:true,
scale: true,
splitNumber: 10,
},
],
series: [
{
name: `Ask A (${quoteData.brokerA}/${quoteData.symbolA})`,
type: 'line',
data: datasetSource
.filter(record => record.askA !== null)
.map(record => [record.timestamp, record.askA]),
yAxisIndex: 0,
lineStyle: { color: '#007bff', width: 2 },
itemStyle: { color: '#007bff' },
showSymbol: true,
symbol: 'circle',
symbolSize: 4,
connectNulls: false,
},
{
name: `Bid A (${quoteData.brokerA}/${quoteData.symbolA})`,
type: 'line',
data: datasetSource
.filter(record => record.bidA !== null)
.map(record => [record.timestamp, record.bidA]),
yAxisIndex: 0,
lineStyle: { color: '#00b7eb', type: 'dashed', width: 2 },
itemStyle: { color: '#00b7eb' },
showSymbol: true,
symbol: 'circle',
symbolSize: 4,
connectNulls: false,
},
{
name: `Ask B (${quoteData.brokerB}/${quoteData.symbolB})`,
type: 'line',
data: datasetSource
.filter(record => record.askB !== null)
.map(record => [record.timestamp, record.askB]),
yAxisIndex: 1,
lineStyle: { color: '#ff4500', width: 2 },
itemStyle: { color: '#ff4500' },
showSymbol: true,
symbol: 'square',
symbolSize: 4,
connectNulls: false,
},
{
name: `Bid B (${quoteData.brokerB}/${quoteData.symbolB})`,
type: 'line',
data: datasetSource
.filter(record => record.bidB !== null)
.map(record => [record.timestamp, record.bidB]),
yAxisIndex: 1,
lineStyle: { color: '#ff8c00', type: 'dashed', width: 2 },
itemStyle: { color: '#ff8c00' },
showSymbol: true,
symbol: 'square',
symbolSize: 4,
connectNulls: false,
},
{
name: `Midline A (${quoteData.brokerA}/${quoteData.symbolA})`,
type: 'line',
data: datasetSource
.filter(record => record.midlineA !== null)
.map(record => [record.timestamp, record.midlineA]),
yAxisIndex: 0,
lineStyle: { color: '#28a745', width: 2 },
itemStyle: { color: '#28a745' },
showSymbol: true,
symbol: 'circle',
symbolSize: 4,
connectNulls: false,
},
{
name: `Midline B (${quoteData.brokerB}/${quoteData.symbolB})`,
type: 'line',
data: datasetSource
.filter(record => record.midlineB !== null)
.map(record => [record.timestamp, record.midlineB]),
yAxisIndex: 1,
lineStyle: { color: '#9932cc', width: 2 },
itemStyle: { color: '#9932cc' },
showSymbol: true,
symbol: 'square',
symbolSize: 4,
connectNulls: false,
},
],
};
return (
<div className= "p-4">
<h2 className="text-2xl font-bold text-gray-800 dark:text-gray-500 mb-4">Quote Chart</h2>
<div className="flex space-x-4 mb-4">
<label className="flex items-center space-x-2 text-gray-700 dark:text-gray-500">
<input
type="radio"
name="chartMode"
checked={!showMidline}
onChange={() => setShowMidline(false)}
className="form-radio text-blue-500 focus:ring-blue-500"
/>
<span>Show Ask/Bid</span>
</label>
<label className="flex items-center space-x-2 text-gray-700 dark:text-gray-500">
<input
type="radio"
name="chartMode"
checked={showMidline}
onChange={() => setShowMidline(true)}
className="form-radio text-blue-500 focus:ring-blue-500"
/>
<span>Show Midline</span>
</label>
</div>
<ReactECharts
echarts={echarts}
ref = {chartRef}
option={option}
style={{ height: 600, width: '100%' }}
opts={{ renderer: 'canvas' }}
theme="dark"
/>
</div>
)
};
export default ChartComponents;

View File

@ -0,0 +1,111 @@
import React from "react";
import { useQueryContext } from "../context/querycontext";
const DropDownContexts: React.FC = () => {
const { queryOptions, brokers, handleBrokerChange, isLoadingBrokers, isErrorBrokers } = useQueryContext();
const brokerList = Object.keys(brokers || {});
const symbolsA = queryOptions.brokerA && brokers[queryOptions.brokerA] ?
brokers[queryOptions.brokerA] : [];
const symbolsB = queryOptions.brokerB && brokers[queryOptions.brokerB] ?
brokers[queryOptions.brokerB] : [];
const onBrokerChange = (key: string, value: string) => {
if (key === 'brokerA') {
const newSymbolA = symbolsA.includes(queryOptions.symbolA) ? queryOptions.symbolA : '';
handleBrokerChange({ ...queryOptions, brokerA: value, symbolA: newSymbolA });
} else if (key === 'brokerB') {
const newSymbolB = symbolsB.includes(queryOptions.symbolB) ? queryOptions.symbolB : '';
handleBrokerChange({ ...queryOptions, brokerB: value, symbolB: newSymbolB });
} else {
handleBrokerChange({ ...queryOptions, [key]: value });
}
};
if (isLoadingBrokers) return <div className="text-center text-gray-500">Loading brokers...</div>;
if (isErrorBrokers) return <div className="text-center text-red-500">Error loading brokers</div>;
return (
<div className="p-4">
<h2 className="text-2xl font-bold text-gray-800 dark:text-gray-500 mb-4">Broker and Symbol Selection</h2>
<div className="space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:space-x-4">
<label htmlFor="brokerA" className="text-gray-700 dark:text-gray-500 font-medium mb-1 sm:mb-0">
Broker A:
</label>
<select
id="brokerA"
value={queryOptions.brokerA}
onChange={(e) => onBrokerChange('brokerA', e.target.value)}
className="form-select bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-500 border border-gray-300 dark:border-gray-700 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full sm:w-48"
>
<option value="">-- Select Broker A --</option>
{brokerList.map((broker) => (
<option key={broker} value={broker}>
{broker}
</option>
))}
</select>
<label htmlFor="symbolA" className="text-gray-700 dark:text-gray-500 font-medium mb-1 sm:mb-0">
Symbol A:
</label>
<select
id="symbolA"
value={queryOptions.symbolA}
onChange={(e) => onBrokerChange('symbolA', e.target.value)}
className="form-select bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-500 border border-gray-300 dark:border-gray-700 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full sm:w-48"
disabled={!queryOptions.brokerA}
>
<option value="">-- Select Symbol A --</option>
{symbolsA.map((s) => (
<option key={s} value={s}>
{s}
</option>
))}
</select>
</div>
<div className="flex flex-col sm:flex-row sm:items-center sm:space-x-4">
<label htmlFor="brokerB" className="text-gray-700 dark:text-gray-500 font-medium mb-1 sm:mb-0">
Broker B:
</label>
<select
id="brokerB"
value={queryOptions.brokerB}
onChange={(e) => onBrokerChange('brokerB', e.target.value)}
className="form-select bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-500 border border-gray-300 dark:border-gray-700 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full sm:w-48"
>
<option value="">-- Select Broker B --</option>
{brokerList.map((b) => (
<option key={b} value={b}>
{b}
</option>
))}
</select>
<label htmlFor="symbolB" className="text-gray-700 dark:text-gray-500 font-medium mb-1 sm:mb-0">
Symbol B:
</label>
<select
id="symbolB"
value={queryOptions.symbolB}
onChange={(e) => onBrokerChange('symbolB', e.target.value)}
className="form-select bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-500 border border-gray-300 dark:border-gray-700 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full sm:w-48"
disabled={!queryOptions.brokerB}
>
<option value="">-- Select Symbol B --</option>
{symbolsB.map((s) => (
<option key={s} value={s}>
{s}
</option>
))}
</select>
</div>
</div>
</div>
)
}
export default DropDownContexts;

View File

@ -0,0 +1,90 @@
import React from "react";
import { useQuoteData } from "../QueryOptions/apiQueryOptions";
import { useQueryContext } from "../context/querycontext";
const TableComponents: React.FC = () => {
const { queryOptions, isLoadingBrokers, isErrorBrokers } =
useQueryContext();
const { data: quoteData, isLoading: isLoadingQuotes, isError: isErrorQuotes } =
useQuoteData(queryOptions);
console.log('quote data before render:', quoteData,
'isLoadingQuotes:', isLoadingQuotes,
'quotesError:', isErrorQuotes);
if (isLoadingBrokers) return <div>Loading brokers...</div>;
if (isErrorBrokers) return <div>Error loading brokers</div>;
return (
<div>
<h2 className="text-2xl font-bold text-gray-800 dark:text-gray-500 mb-4">Quote Data</h2>
{isLoadingQuotes && <div className="text-center text-gray-500">Loading quote data...</div>}
{isErrorQuotes && <div className="text-center text-red-500">Error loading quote data</div>}
{!isLoadingQuotes && !isErrorQuotes && !quoteData && <div className="text-center text-gray-500">No quote data available</div>}
{quoteData && (
<div className="flex flex-row gap-4">
<table className="flex-1 min-w-0 border-collapse border border-gray-300 dark:border-gray-700">
<thead>
<tr className="bg-white dark:bg-gray-800">
<th className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-left text-gray-700 dark:text-gray-300">Timestamp</th>
<th className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-left text-gray-700 dark:text-gray-300">Ask A ({quoteData.brokerA}/{quoteData.symbolA})</th>
<th className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-left text-gray-700 dark:text-gray-300">Bid A</th>
<th className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-left text-gray-700 dark:text-gray-300">Spread A</th>
<th className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-left text-gray-700 dark:text-gray-300">Midline A</th>
</tr>
</thead>
<tbody>
{quoteData.records
.filter(record =>
record.askA !== null ||
record.bidA !== null ||
record.spreadA !== null ||
record.midlineA !== null
)
.map((record, index) => (
<tr key = {index} className="bg-white dark:bg-gray-800">
<td className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-gray-700 dark:text-gray-300">{record.timestamp}</td>
<td className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-gray-700 dark:text-gray-300">{record.askA}</td>
<td className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-gray-700 dark:text-gray-300">{record.bidA}</td>
<td className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-gray-700 dark:text-gray-300">{record.spreadA}</td>
<td className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-gray-700 dark:text-gray-300">{record.midlineA}</td>
</tr>
))}
</tbody>
</table>
<table className="flex-1 min-w-0 border-collapse border border-gray-300 dark:border-gray-700">
<thead>
<tr className="bg-white dark:bg-gray-800">
<th className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-left text-gray-700 dark:text-gray-300">Timestamp</th>
<th className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-left text-gray-700 dark:text-gray-300">Ask B ({quoteData.brokerB}/{quoteData.symbolB})</th>
<th className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-left text-gray-700 dark:text-gray-300">Bid B</th>
<th className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-left text-gray-700 dark:text-gray-300">Spread B</th>
<th className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-left text-gray-700 dark:text-gray-300">Midline B</th>
</tr>
</thead>
<tbody>
{quoteData.records
.filter(record =>
record.askB !== null ||
record.bidB !== null ||
record.spreadB !== null ||
record.midlineB !== null
)
.map((record, index) => (
<tr key = {index} className="bg-white dark:bg-gray-800">
<td className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-gray-700 dark:text-gray-300">{record.timestamp}</td>
<td className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-gray-700 dark:text-gray-300">{record.askB}</td>
<td className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-gray-700 dark:text-gray-300">{record.bidB}</td>
<td className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-gray-700 dark:text-gray-300">{record.spreadB}</td>
<td className="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-4 py-2 text-gray-700 dark:text-gray-300">{record.midlineB}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
};
export default TableComponents;

View File

@ -0,0 +1,42 @@
import React, { useState, useContext } from "react";
import { type QueryOptions, type QueryContextType } from "../QueryOptions/dataTypes";
import { useBrokerSymbols } from '../QueryOptions/apiQueryOptions';
const QueryContext = React.createContext<QueryContextType |
undefined>(undefined);
export const QueryContextProvider: React.FC<{ children: React.ReactNode }> = ({children }) => {
const [queryOptions, setQueryOptions] = useState<QueryOptions>({
brokerA: "",
symbolA: "",
brokerB: "",
symbolB: ""
}
);
const { data: brokersData, isLoading: isLoadingBrokers, isError: isErrorBrokers, error: brokersError } = useBrokerSymbols();
const handleBrokerChange = (newOptions: Partial<QueryOptions>) => {
setQueryOptions((prev) => ({ ...prev, ...newOptions }));
};
return (
<QueryContext.Provider
value={{
queryOptions,
setQueryOptions,
brokers: brokersData?.brokers || {},
brokersError,
handleBrokerChange,
isLoadingBrokers,
isErrorBrokers,
}}
>
{children}
</QueryContext.Provider>
);
};
export const useQueryContext = (): QueryContextType => {
const context = useContext(QueryContext);
if (!context) throw new Error('useQueryContext must be used within a QueryProvider');
return context;
};

View File

@ -0,0 +1,5 @@
@import "tailwindcss";

View File

@ -0,0 +1,15 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css'
import App from './App.tsx';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</StrictMode>,
);

View File

@ -0,0 +1,11 @@
{
"folders": [
{
"path": ".."
},
{
"path": "../../quote_plotter_client"
}
],
"settings": {}
}

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react(),
tailwindcss(),
],
})