document.querySelector("#fetch")!.addEventListener("click", async () => { const username = (document.querySelector("#username") as HTMLInputElement).value; const apiKey = (document.querySelector("#api-key") as HTMLInputElement).value; const supplyForDays = parseInt((document.querySelector("#days") as HTMLInputElement).value, 10); const output = await calculate(username, apiKey, supplyForDays); console.log(output); }); async function calculate(username: string, apiKey: string, supplyForDays: number): Promise { const [prices, planets, warehouse, {bids, orders}] = await Promise.all([ getPrices(), getPlanets(username, apiKey), warehouseInventory(username, apiKey), getBids(username, apiKey) ]); const buy = new Map(); for (const planet of planets) for (const [mat, amount] of planet.supplyForDays(supplyForDays)) buy.set(mat, (buy.get(mat) ?? 0) + amount); // what's left to buy const materials: Material[] = []; for (const [mat, amount] of buy) { const remaining = Math.max(amount - (bids.get(mat) ?? 0) - (warehouse.get(mat) ?? 0), 0); const price = prices.get(mat); if (!price || price.Bid === null || price.Ask === null) { console.log(mat, 'has no bid/ask'); continue; } const spread = price.Ask - price.Bid; materials.push({ ticker: mat, amount, bids: bids.get(mat) ?? 0, warehouse: warehouse.get(mat) ?? 0, spread, savings: spread * remaining, }); } materials.sort((a, b) => b.savings - a.savings); const tbody = document.querySelector("tbody")!; tbody.innerHTML = ''; const format= new Intl.NumberFormat(undefined, {maximumFractionDigits: 0}).format; for (const m of materials) { const tr = document.createElement("tr"); const buyAmount = Math.max(m.amount - m.bids - m.warehouse, 0); tr.innerHTML = ` ${m.ticker} ${format(m.amount)} ${format(m.bids)} ${format(m.warehouse)} ${format(buyAmount)} ${format(m.spread)} ${format(m.savings)} `; tbody.appendChild(tr); } // deposits of current bids orders.sort((a, b) => (b.Limit * b.Amount) - (a.Limit * a.Amount)); for (const order of orders) { const deposit = order.Limit * order.Amount; console.log(`${order.MaterialTicker.padEnd(4)} ${deposit}\n`); } } async function getPrices() { const rawPrices= await fetch('https://refined-prun.github.io/refined-prices/all.json').then(r => r.json()); const prices = new Map(); for (const p of rawPrices) if (p.ExchangeCode === 'IC1') prices.set(p.MaterialTicker, p); return prices; } async function getPlanets(username: string, apiKey: string) { const fioBurns: FIOBurn[] = await fetch('https://rest.fnar.net/fioweb/burn/user/' + username, {headers: {'Authorization': apiKey}}).then(r => r.json()); const planets = fioBurns.map(burn => new Planet(burn)); return planets; } async function warehouseInventory(username: string, apiKey: string): Promise> { const warehouses: Warehouse[] = await fetch('https://rest.fnar.net/sites/warehouses/' + username, {headers: {'Authorization': apiKey}}).then(r => r.json()); for (const warehouse of warehouses) if (warehouse.LocationNaturalId === 'HRT') { const storage: Storage = await fetch(`https://rest.fnar.net/storage/${username}/${warehouse.StoreId}`, {headers: {'Authorization': apiKey}}).then(r => r.json()); const inventory = new Map(); for (const item of storage.StorageItems) inventory.set(item.MaterialTicker, item.MaterialAmount); return inventory; } throw new Error("couldn't find HRT warehouse"); } async function getBids(username: string, apiKey: string) { const allOrders: ExchangeOrder[] = await fetch('https://rest.fnar.net/cxos/' + username, {headers: {'Authorization': apiKey}}).then(r => r.json()); const orders = allOrders.filter(order => order.OrderType === 'BUYING' && order.Status !== 'FILLED' && order.ExchangeCode === 'IC1'); const bids = new Map(); for (const order of orders) bids.set(order.MaterialTicker, (bids.get(order.MaterialTicker) ?? 0) + order.Amount); return {bids, orders}; } class Planet { name: string; inventory: Map; netConsumption: Amount[]; constructor(fioBurn: FIOBurn) { this.name = fioBurn.PlanetName || fioBurn.PlanetNaturalId; this.inventory = new Map(); for (const item of fioBurn.Inventory) this.inventory.set(item.MaterialTicker, item.MaterialAmount); const producing = new Map(); for (const item of fioBurn.OrderProduction) producing.set(item.MaterialTicker, item); this.netConsumption = []; for (const c of [...fioBurn.OrderConsumption, ...fioBurn.WorkforceConsumption]) { let net = c.DailyAmount; const production = producing.get(c.MaterialTicker); if (production) { net -= production.DailyAmount; if (net < 0) continue; } c.netConsumption = net; this.netConsumption.push(c); } } supplyForDays(targetDays: number): Map { const buy = new Map(); for (const consumption of this.netConsumption) { const ticker = consumption.MaterialTicker; const avail = this.inventory.get(ticker) ?? 0; const dailyConsumption = consumption.netConsumption!; const days = avail / dailyConsumption; if (days < targetDays) buy.set(ticker, Math.ceil((targetDays - days) * dailyConsumption)); } return buy; } } interface Material { ticker: string; amount: number; bids: number; warehouse: number; spread: number; savings: number; } interface RawPrice { MaterialTicker: string; ExchangeCode: string; Bid: number | null; Ask: number | null; FullTicker: string; } interface ExchangeOrder { MaterialTicker: string; ExchangeCode: string; OrderType: 'SELLING' | 'BUYING'; Status: 'FILLED' | 'PARTIALLY_FILLED'; Amount: number; Limit: number; } interface StorageItem { MaterialTicker: string; MaterialAmount: number; } interface Storage { Name: string; StorageItems: StorageItem[]; WeightLoad: number; VolumeLoad: number; Type: 'STORE' | 'WAREHOUSE_STORE' | 'FTL_FUEL_STORE' | 'STL_FUEL_STORE' | 'SHIP_STORE'; } interface Warehouse { StoreId: string; LocationNaturalId: string; } interface Amount { MaterialTicker: string; DailyAmount: number; netConsumption?: number; } interface FIOBurn { PlanetName: string; PlanetNaturalId: string; Error: any; OrderConsumption: Amount[]; WorkforceConsumption: Amount[]; Inventory: StorageItem[]; OrderProduction: Amount[]; }