initial commit
This commit is contained in:
commit
55475ff7e5
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal 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
5
Quote_Manager_server/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
quotes.db
|
||||
archives/
|
||||
31
Quote_Manager_server/api_main.py
Normal file
31
Quote_Manager_server/api_main.py
Normal 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)
|
||||
31
Quote_Manager_server/batch_ingest.py
Normal file
31
Quote_Manager_server/batch_ingest.py
Normal 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()
|
||||
70
Quote_Manager_server/cli_main.py
Normal file
70
Quote_Manager_server/cli_main.py
Normal 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}")
|
||||
|
||||
|
||||
68
Quote_Manager_server/ingest.py
Normal file
68
Quote_Manager_server/ingest.py
Normal 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}")
|
||||
79
Quote_Manager_server/quote_contracts.py
Normal file
79
Quote_Manager_server/quote_contracts.py
Normal 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]
|
||||
|
||||
267
Quote_Manager_server/quote_db.py
Normal file
267
Quote_Manager_server/quote_db.py
Normal 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()
|
||||
223
Quote_Manager_server/quote_service.py
Normal file
223
Quote_Manager_server/quote_service.py
Normal 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]
|
||||
|
||||
|
||||
|
||||
BIN
Quote_Manager_server/quotes.db-journal
Normal file
BIN
Quote_Manager_server/quotes.db-journal
Normal file
Binary file not shown.
23
Quote_Manager_server/requirements.txt
Normal file
23
Quote_Manager_server/requirements.txt
Normal 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
|
||||
23
Quote_Manager_server/reset_db.py
Normal file
23
Quote_Manager_server/reset_db.py
Normal 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)
|
||||
|
||||
74
Quote_Manager_server/routes.py
Normal file
74
Quote_Manager_server/routes.py
Normal 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
24
quote_plotter_client/.gitignore
vendored
Normal 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?
|
||||
23
quote_plotter_client/eslint.config.js
Normal file
23
quote_plotter_client/eslint.config.js
Normal 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,
|
||||
},
|
||||
},
|
||||
])
|
||||
13
quote_plotter_client/index.html
Normal file
13
quote_plotter_client/index.html
Normal 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
3797
quote_plotter_client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
quote_plotter_client/package.json
Normal file
34
quote_plotter_client/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
4
quote_plotter_client/src/App.css
Normal file
4
quote_plotter_client/src/App.css
Normal file
@ -0,0 +1,4 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
|
||||
|
||||
23
quote_plotter_client/src/App.tsx
Normal file
23
quote_plotter_client/src/App.tsx
Normal 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;
|
||||
110
quote_plotter_client/src/QueryOptions/apiQueryOptions.tsx
Normal file
110
quote_plotter_client/src/QueryOptions/apiQueryOptions.tsx
Normal 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",
|
||||
});
|
||||
};
|
||||
50
quote_plotter_client/src/QueryOptions/dataTypes.tsx
Normal file
50
quote_plotter_client/src/QueryOptions/dataTypes.tsx
Normal 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;
|
||||
}
|
||||
326
quote_plotter_client/src/components/chartComponents.tsx
Normal file
326
quote_plotter_client/src/components/chartComponents.tsx
Normal 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;
|
||||
111
quote_plotter_client/src/components/dropDownComponents.tsx
Normal file
111
quote_plotter_client/src/components/dropDownComponents.tsx
Normal 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;
|
||||
90
quote_plotter_client/src/components/tableComponents.tsx
Normal file
90
quote_plotter_client/src/components/tableComponents.tsx
Normal 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;
|
||||
42
quote_plotter_client/src/context/querycontext.tsx
Normal file
42
quote_plotter_client/src/context/querycontext.tsx
Normal 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;
|
||||
};
|
||||
5
quote_plotter_client/src/index.css
Normal file
5
quote_plotter_client/src/index.css
Normal file
@ -0,0 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
|
||||
|
||||
|
||||
15
quote_plotter_client/src/main.tsx
Normal file
15
quote_plotter_client/src/main.tsx
Normal 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>,
|
||||
);
|
||||
11
quote_plotter_client/src/quote_plotter_vite.code-workspace
Normal file
11
quote_plotter_client/src/quote_plotter_vite.code-workspace
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
},
|
||||
{
|
||||
"path": "../../quote_plotter_client"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
27
quote_plotter_client/tsconfig.app.json
Normal file
27
quote_plotter_client/tsconfig.app.json
Normal 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"]
|
||||
}
|
||||
7
quote_plotter_client/tsconfig.json
Normal file
7
quote_plotter_client/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
25
quote_plotter_client/tsconfig.node.json
Normal file
25
quote_plotter_client/tsconfig.node.json
Normal 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"]
|
||||
}
|
||||
10
quote_plotter_client/vite.config.ts
Normal file
10
quote_plotter_client/vite.config.ts
Normal 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(),
|
||||
],
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user