|
|
@@ -0,0 +1,213 @@
|
|
|
+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<void> {
|
|
|
+ const [prices, planets, warehouse, {bids, orders}] = await Promise.all([
|
|
|
+ getPrices(),
|
|
|
+ getPlanets(username, apiKey),
|
|
|
+ warehouseInventory(username, apiKey),
|
|
|
+ getBids(username, apiKey)
|
|
|
+ ]);
|
|
|
+ const buy = new Map<string, number>();
|
|
|
+ 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 = `
|
|
|
+ <td>${m.ticker}</td>
|
|
|
+ <td>${format(m.amount)}</td>
|
|
|
+ <td>${format(m.bids)}</td>
|
|
|
+ <td>${format(m.warehouse)}</td>
|
|
|
+ <td>${format(buyAmount)}</td>
|
|
|
+ <td>${format(m.spread)}</td>
|
|
|
+ <td>${format(m.savings)}</td>
|
|
|
+ `;
|
|
|
+ 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<string, RawPrice>();
|
|
|
+ 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<Map<string, number>> {
|
|
|
+ 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<string, number>();
|
|
|
+ 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<string, number>();
|
|
|
+ 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<string, number>;
|
|
|
+ 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<string, Amount>();
|
|
|
+ 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<string, number> {
|
|
|
+ const buy = new Map<string, number>();
|
|
|
+ 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[];
|
|
|
+}
|