buy.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import {setupPopover} from './popover';
  2. document.querySelector("#fetch")!.addEventListener("click", async () => {
  3. const username = (document.querySelector("#username") as HTMLInputElement).value;
  4. const apiKey = (document.querySelector("#api-key") as HTMLInputElement).value;
  5. const supplyForDays = parseInt((document.querySelector("#days") as HTMLInputElement).value, 10);
  6. const output = await calculate(username, apiKey, supplyForDays);
  7. console.log(output);
  8. });
  9. setupPopover();
  10. async function calculate(username: string, apiKey: string, supplyForDays: number): Promise<void> {
  11. const [prices, planets, warehouse, {bids, orders}] = await Promise.all([
  12. getPrices(),
  13. getPlanets(username, apiKey),
  14. warehouseInventory(username, apiKey),
  15. getBids(username, apiKey)
  16. ]);
  17. const buy = new Map<string, number>();
  18. for (const planet of planets)
  19. for (const [mat, amount] of planet.supplyForDays(supplyForDays))
  20. buy.set(mat, (buy.get(mat) ?? 0) + amount);
  21. // what's left to buy
  22. const materials: Material[] = [];
  23. for (const [mat, amount] of buy) {
  24. const remaining = Math.max(amount - (bids.get(mat) ?? 0) - (warehouse.get(mat) ?? 0), 0);
  25. const price = prices.get(mat);
  26. if (!price || price.Bid === null || price.Ask === null) {
  27. console.log(mat, 'has no bid/ask');
  28. continue;
  29. }
  30. const spread = price.Ask - price.Bid;
  31. materials.push({
  32. ticker: mat,
  33. amount,
  34. bids: bids.get(mat) ?? 0,
  35. warehouse: warehouse.get(mat) ?? 0,
  36. spread,
  37. savings: spread * remaining,
  38. });
  39. }
  40. materials.sort((a, b) => b.savings - a.savings);
  41. const tbody = document.querySelector("tbody")!;
  42. tbody.innerHTML = '';
  43. const format= new Intl.NumberFormat(undefined, {maximumFractionDigits: 0}).format;
  44. for (const m of materials) {
  45. const tr = document.createElement("tr");
  46. const buyAmount = Math.max(m.amount - m.bids - m.warehouse, 0);
  47. tr.innerHTML = `
  48. <td>${m.ticker}</td>
  49. <td>${format(m.amount)}</td>
  50. <td>${format(m.bids)}</td>
  51. <td>${format(m.warehouse)}</td>
  52. <td>${format(buyAmount)}</td>
  53. <td>${format(m.spread)}</td>
  54. <td>${format(m.savings)}</td>
  55. `;
  56. tbody.appendChild(tr);
  57. }
  58. // deposits of current bids
  59. orders.sort((a, b) => (b.Limit * b.Amount) - (a.Limit * a.Amount));
  60. for (const order of orders) {
  61. const deposit = order.Limit * order.Amount;
  62. console.log(`${order.MaterialTicker.padEnd(4)} ${deposit}\n`);
  63. }
  64. }
  65. async function getPrices() {
  66. const rawPrices= await fetch('https://refined-prun.github.io/refined-prices/all.json').then(r => r.json());
  67. const prices = new Map<string, RawPrice>();
  68. for (const p of rawPrices)
  69. if (p.ExchangeCode === 'IC1')
  70. prices.set(p.MaterialTicker, p);
  71. return prices;
  72. }
  73. async function getPlanets(username: string, apiKey: string) {
  74. const fioBurns: FIOBurn[] = await fetch('https://rest.fnar.net/fioweb/burn/user/' + username,
  75. {headers: {'Authorization': apiKey}}).then(r => r.json());
  76. const planets = fioBurns.map(burn => new Planet(burn));
  77. return planets;
  78. }
  79. async function warehouseInventory(username: string, apiKey: string): Promise<Map<string, number>> {
  80. const warehouses: Warehouse[] = await fetch('https://rest.fnar.net/sites/warehouses/' + username,
  81. {headers: {'Authorization': apiKey}}).then(r => r.json());
  82. for (const warehouse of warehouses)
  83. if (warehouse.LocationNaturalId === 'HRT') {
  84. const storage: Storage = await fetch(`https://rest.fnar.net/storage/${username}/${warehouse.StoreId}`,
  85. {headers: {'Authorization': apiKey}}).then(r => r.json());
  86. const inventory = new Map<string, number>();
  87. for (const item of storage.StorageItems)
  88. inventory.set(item.MaterialTicker, item.MaterialAmount);
  89. return inventory;
  90. }
  91. throw new Error("couldn't find HRT warehouse");
  92. }
  93. async function getBids(username: string, apiKey: string) {
  94. const allOrders: ExchangeOrder[] = await fetch('https://rest.fnar.net/cxos/' + username,
  95. {headers: {'Authorization': apiKey}}).then(r => r.json());
  96. const orders = allOrders.filter(order =>
  97. order.OrderType === 'BUYING' && order.Status !== 'FILLED' && order.ExchangeCode === 'IC1');
  98. const bids = new Map<string, number>();
  99. for (const order of orders)
  100. bids.set(order.MaterialTicker, (bids.get(order.MaterialTicker) ?? 0) + order.Amount);
  101. return {bids, orders};
  102. }
  103. class Planet {
  104. name: string;
  105. inventory: Map<string, number>;
  106. netConsumption: Amount[];
  107. constructor(fioBurn: FIOBurn) {
  108. this.name = fioBurn.PlanetName || fioBurn.PlanetNaturalId;
  109. this.inventory = new Map();
  110. for (const item of fioBurn.Inventory)
  111. this.inventory.set(item.MaterialTicker, item.MaterialAmount);
  112. const producing = new Map<string, Amount>();
  113. for (const item of fioBurn.OrderProduction)
  114. producing.set(item.MaterialTicker, item);
  115. this.netConsumption = [];
  116. for (const c of [...fioBurn.OrderConsumption, ...fioBurn.WorkforceConsumption]) {
  117. let net = c.DailyAmount;
  118. const production = producing.get(c.MaterialTicker);
  119. if (production) {
  120. net -= production.DailyAmount;
  121. if (net < 0)
  122. continue;
  123. }
  124. c.netConsumption = net;
  125. this.netConsumption.push(c);
  126. }
  127. }
  128. supplyForDays(targetDays: number): Map<string, number> {
  129. const buy = new Map<string, number>();
  130. for (const consumption of this.netConsumption) {
  131. const ticker = consumption.MaterialTicker;
  132. const avail = this.inventory.get(ticker) ?? 0;
  133. const dailyConsumption = consumption.netConsumption!;
  134. const days = avail / dailyConsumption;
  135. if (days < targetDays)
  136. buy.set(ticker, Math.ceil((targetDays - days) * dailyConsumption));
  137. }
  138. return buy;
  139. }
  140. }
  141. interface Material {
  142. ticker: string;
  143. amount: number;
  144. bids: number;
  145. warehouse: number;
  146. spread: number;
  147. savings: number;
  148. }
  149. interface RawPrice {
  150. MaterialTicker: string;
  151. ExchangeCode: string;
  152. Bid: number | null;
  153. Ask: number | null;
  154. FullTicker: string;
  155. }
  156. interface ExchangeOrder {
  157. MaterialTicker: string;
  158. ExchangeCode: string;
  159. OrderType: 'SELLING' | 'BUYING';
  160. Status: 'FILLED' | 'PARTIALLY_FILLED';
  161. Amount: number;
  162. Limit: number;
  163. }
  164. interface StorageItem {
  165. MaterialTicker: string;
  166. MaterialAmount: number;
  167. }
  168. interface Storage {
  169. Name: string;
  170. StorageItems: StorageItem[];
  171. WeightLoad: number;
  172. VolumeLoad: number;
  173. Type: 'STORE' | 'WAREHOUSE_STORE' | 'FTL_FUEL_STORE' | 'STL_FUEL_STORE' | 'SHIP_STORE';
  174. }
  175. interface Warehouse {
  176. StoreId: string;
  177. LocationNaturalId: string;
  178. }
  179. interface Amount {
  180. MaterialTicker: string;
  181. DailyAmount: number;
  182. netConsumption?: number;
  183. }
  184. interface FIOBurn {
  185. PlanetName: string;
  186. PlanetNaturalId: string;
  187. Error: any;
  188. OrderConsumption: Amount[];
  189. WorkforceConsumption: Amount[];
  190. Inventory: StorageItem[];
  191. OrderProduction: Amount[];
  192. }