|
@@ -27,9 +27,10 @@ tickerSelect.addEventListener('change', async () => {
|
|
|
|
|
|
|
|
async function render() {
|
|
async function render() {
|
|
|
charts.innerHTML = '';
|
|
charts.innerHTML = '';
|
|
|
|
|
+ const ticker = tickerSelect.value;
|
|
|
const cxpc = await Promise.all([
|
|
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;
|
|
let minDate = null, maxDate = null;
|
|
@@ -43,12 +44,14 @@ async function render() {
|
|
|
}
|
|
}
|
|
|
if (minDate === null || maxDate === null)
|
|
if (minDate === null || maxDate === null)
|
|
|
throw new Error('no data');
|
|
throw new Error('no data');
|
|
|
-
|
|
|
|
|
const dateRange: [Date, Date] = [new Date(minDate), new Date(maxDate)];
|
|
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]),
|
|
|
|
|
+ renderRecipes(ticker),
|
|
|
|
|
+ ]));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function getCXPC(ticker: string, cx: string): Promise<PriceChartPoint[]> {
|
|
async function getCXPC(ticker: string, cx: string): Promise<PriceChartPoint[]> {
|
|
@@ -57,7 +60,14 @@ async function getCXPC(ticker: string, cx: string): Promise<PriceChartPoint[]> {
|
|
|
return cxpc.filter((p) => p.Interval === 'HOUR_TWELVE' && p.DateEpochMs > threshold);
|
|
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 {
|
|
SVGSVGElement | HTMLElement {
|
|
|
const chartsWidth = charts.getBoundingClientRect().width;
|
|
const chartsWidth = charts.getBoundingClientRect().width;
|
|
|
const numFormat = Plot.formatNumber();
|
|
const numFormat = Plot.formatNumber();
|
|
@@ -116,6 +126,64 @@ 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;
|
|
|
|
|
+
|
|
|
|
|
+async function renderRecipes(ticker: string): Promise<HTMLElement> {
|
|
|
|
|
+ const recipes: Recipe[] = await cachedFetchJSON(`https://rest.fnar.net/recipes/${ticker}`);
|
|
|
|
|
+
|
|
|
|
|
+ const section = document.createElement('section');
|
|
|
|
|
+ for (const recipe of recipes)
|
|
|
|
|
+ section.innerHTML += `<div class="recipe">
|
|
|
|
|
+ ${recipe.Inputs.map((i) => `${i.Amount}×${i.CommodityTicker}`).join(' ')}
|
|
|
|
|
+ <span class="building">${recipe.BuildingTicker}</span>
|
|
|
|
|
+ ${recipe.Outputs.map((o) => `${o.Amount}×${o.CommodityTicker}`).join(' ')}
|
|
|
|
|
+ </div>`;
|
|
|
|
|
+ return section;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
interface Material {
|
|
interface Material {
|
|
|
Ticker: string;
|
|
Ticker: string;
|
|
|
Name: string;
|
|
Name: string;
|
|
@@ -129,3 +197,34 @@ interface PriceChartPoint {
|
|
|
Volume: number;
|
|
Volume: number;
|
|
|
Traded: 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;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface Recipe {
|
|
|
|
|
+ BuildingTicker: string;
|
|
|
|
|
+ Inputs: RecipeMat[];
|
|
|
|
|
+ Outputs: RecipeMat[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface RecipeMat {
|
|
|
|
|
+ CommodityTicker: string;
|
|
|
|
|
+ Amount: number;
|
|
|
|
|
+}
|