raylu пре 3 дана
родитељ
комит
726c1a292e
2 измењених фајлова са 97 додато и 8 уклоњено
  1. 82 8
      ts/mat.ts
  2. 15 0
      www/style.css

+ 82 - 8
ts/mat.ts

@@ -27,9 +27,10 @@ tickerSelect.addEventListener('change', async () => {
 	
 async function render() {
 	charts.innerHTML = '';
+	const ticker = tickerSelect.value;
 	const cxpc = await Promise.all([
-		getCXPC(tickerSelect.value, 'NC1'), getCXPC(tickerSelect.value, 'CI1'),
-		getCXPC(tickerSelect.value, 'IC1'), getCXPC(tickerSelect.value, 'AI1'),
+		getCXPC(ticker, 'NC1'), getCXPC(ticker, 'CI1'),
+		getCXPC(ticker, 'IC1'), getCXPC(ticker, 'AI1'),
 	]);
 
 	let minDate = null, maxDate = null;
@@ -43,12 +44,13 @@ async function render() {
 		}
 	if (minDate === null || maxDate === null)
 		throw new Error('no data');
-
 	const dateRange: [Date, Date] = [new Date(minDate), new Date(maxDate)];
-	charts.append(
-		renderPriceChart('NC1', dateRange, maxPrice, maxTraded, cxpc[0]), renderPriceChart('CI1', dateRange, maxPrice, maxTraded, cxpc[1]),
-		renderPriceChart('IC1', dateRange, maxPrice, maxTraded, cxpc[2]), renderPriceChart('AI1', dateRange, maxPrice, maxTraded, cxpc[3]),
-	);
+	const cxpcRange = {dateRange, maxPrice, maxTraded};
+
+	charts.append(...await Promise.all([
+		renderExchange('NC1', ticker, cxpcRange, cxpc[0]), renderExchange('CI1', ticker, cxpcRange, cxpc[1]),
+		renderExchange('IC1', ticker, cxpcRange, cxpc[2]), renderExchange('AI1', ticker, cxpcRange, cxpc[3]),
+	]));
 }
 
 async function getCXPC(ticker: string, cx: string): Promise<PriceChartPoint[]> {
@@ -57,7 +59,14 @@ async function getCXPC(ticker: string, cx: string): Promise<PriceChartPoint[]> {
 	return cxpc.filter((p) => p.Interval === 'HOUR_TWELVE' && p.DateEpochMs > threshold);
 }
 
-function renderPriceChart(cx: string, dateRange: [Date, Date], maxPrice: number, maxTraded: number, cxpc: PriceChartPoint[]):
+async function renderExchange(cx: string, ticker: string, cxpcRange: CXPCRange, cxpc: PriceChartPoint[]): Promise<HTMLElement> {
+	const div = document.createElement('div');
+	div.append(renderPriceChart(cx, cxpcRange, cxpc));
+	div.append(renderOrderBook(await cachedFetchJSON(`https://rest.fnar.net/exchange/${ticker}.${cx}`)));
+	return div;
+}
+
+function renderPriceChart(cx: string, {dateRange, maxPrice, maxTraded}: CXPCRange, cxpc: PriceChartPoint[]):
 		SVGSVGElement | HTMLElement {
 	const chartsWidth = charts.getBoundingClientRect().width;
 	const numFormat = Plot.formatNumber();
@@ -116,6 +125,51 @@ function renderPriceChart(cx: string, dateRange: [Date, Date], maxPrice: number,
 	});
 }
 
+function renderOrderBook(book: OrderBook): HTMLElement {
+	const table = document.createElement('table');
+	book.SellingOrders.sort((a, b) => b.ItemCost - a.ItemCost);
+	for (const order of book.SellingOrders) {
+		if (book.MMSell !== null && order.ItemCost >= book.MMSell && order.ItemCount !== null) continue;
+		const row = renderOrder(order);
+		row.classList.add('sell');
+		table.appendChild(row);
+	}
+	book.BuyingOrders.sort((a, b) => b.ItemCost - a.ItemCost);
+	for (const order of book.BuyingOrders) {
+		if (book.MMBuy !== null && order.ItemCost <= book.MMBuy && order.ItemCount !== null) continue;
+		const row = renderOrder(order);
+		row.classList.add('buy');
+		table.appendChild(row);
+	}
+
+	const div = document.createElement('div');
+	div.classList.add('cxob');
+	div.appendChild(table);
+	// center the first bid
+	const firstBuyRow: HTMLTableRowElement | null = table.querySelector('tr.buy');
+	if (firstBuyRow !== null) {
+		const centerFirstBuyOrder = () => {
+			if (div.isConnected)
+				div.scrollTop = Math.max(0, firstBuyRow.offsetTop - div.clientHeight / 2 + 10);
+			else
+				requestAnimationFrame(centerFirstBuyOrder);
+		};
+		requestAnimationFrame(centerFirstBuyOrder);
+	}
+	return div;
+}
+
+function renderOrder(order: Order): HTMLTableRowElement {
+	const row = document.createElement('tr');
+	row.insertCell().textContent = order.CompanyName;
+	row.insertCell().textContent = order.CompanyCode;
+	row.insertCell().textContent = order.ItemCount === null ? '∞' : order.ItemCount.toString();
+	row.insertCell().textContent = `${formatPrice(order.ItemCost)}`;
+	return row;
+}
+
+const formatPrice = new Intl.NumberFormat(undefined, {minimumSignificantDigits: 3, maximumSignificantDigits: 3}).format;
+
 interface Material {
 	Ticker: string;
 	Name: string;
@@ -129,3 +183,23 @@ interface PriceChartPoint {
 	Volume: number;
 	Traded: number;
 }
+
+interface CXPCRange {
+	dateRange: [Date, Date];
+	maxPrice: number;
+	maxTraded: number;
+}
+
+interface OrderBook {
+	BuyingOrders: Order[];
+	SellingOrders: Order[];
+	MMBuy: number | null;
+	MMSell: number | null;
+}
+
+interface Order {
+	CompanyName: string;
+	CompanyCode: string;
+	ItemCount: number | null;
+	ItemCost: number;
+}

+ 15 - 0
www/style.css

@@ -108,15 +108,30 @@ main.buy {
 main.mat {
 	width: 90%;
 	min-width: 500px;
+
 	#charts {
 		display: flex;
 		flex-wrap: wrap;
 		justify-content: space-between;
 		row-gap: 2em;
 		margin-top: 2em;
+
 		svg {
 			display: inline-block;
 		}
+
+		div.cxob {
+			max-height: 10em;
+			overflow-y: scroll;
+			font-size: 12px;
+
+			tr.sell {
+				color: #f80;
+			}
+			tr.buy {
+				color: #0aa;
+			}
+		}
 	}
 }