buy.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. // ts/cache.ts
  2. var fetchCache = new Map;
  3. async function cachedFetchJSON(url, options) {
  4. if (fetchCache.has(url))
  5. return fetchCache.get(url);
  6. const response = await fetch(url, options).then((r) => r.json());
  7. fetchCache.set(url, response);
  8. return response;
  9. }
  10. // ts/popover.ts
  11. function setupPopover() {
  12. const main = document.querySelector("main");
  13. const popover = document.querySelector("#popover");
  14. main.addEventListener("mouseover", (event) => {
  15. const target = event.target;
  16. if (target.dataset.tooltip) {
  17. popover.textContent = target.dataset.tooltip;
  18. const rect = target.getBoundingClientRect();
  19. popover.style.left = `${rect.left}px`;
  20. popover.style.top = `${rect.bottom}px`;
  21. popover.showPopover();
  22. }
  23. });
  24. main.addEventListener("mouseout", (event) => {
  25. const target = event.target;
  26. if (target.dataset.tooltip)
  27. popover.hidePopover();
  28. });
  29. }
  30. // ts/buy.ts
  31. var warehouseNames = {
  32. AI1: "ANT",
  33. CI1: "BEN",
  34. IC1: "HRT",
  35. NC1: "MOR"
  36. };
  37. var username = document.querySelector("#username");
  38. var apiKey = document.querySelector("#api-key");
  39. {
  40. const storedUsername = localStorage.getItem("fio-username");
  41. if (storedUsername)
  42. username.value = storedUsername;
  43. const storedApiKey = localStorage.getItem("fio-api-key");
  44. if (storedApiKey)
  45. apiKey.value = storedApiKey;
  46. }
  47. document.querySelector("#fetch").addEventListener("click", async () => {
  48. const supplyForDays = parseInt(document.querySelector("#days").value, 10);
  49. const loader = document.querySelector("#loader");
  50. loader.innerHTML = "";
  51. loader.style.display = "block";
  52. try {
  53. await calculate(username.value, apiKey.value, supplyForDays);
  54. localStorage.setItem("fio-username", username.value);
  55. localStorage.setItem("fio-api-key", apiKey.value);
  56. } catch (e) {
  57. console.error(e);
  58. }
  59. loader.style.display = "none";
  60. });
  61. document.querySelector("#buys").addEventListener("click", (event) => {
  62. if (!event.target)
  63. return;
  64. const target = event.target;
  65. if (target.tagName !== "INPUT" || target.type !== "button")
  66. return;
  67. const xitSection = target.parentElement;
  68. if (!xitSection || !xitSection.classList.contains("xit-act"))
  69. return;
  70. navigator.clipboard.writeText(xitSection.querySelector("textarea").value);
  71. });
  72. setupPopover();
  73. async function calculate(username2, apiKey2, supplyForDays) {
  74. const buys = document.querySelector("#buys");
  75. buys.innerHTML = "";
  76. const closest = await cachedFetchJSON("/closest.json");
  77. const planetsByCX = new Map;
  78. for (const planet of await getPlanets(username2, apiKey2)) {
  79. const wh = closest[planet.id];
  80. const existing = planetsByCX.get(wh);
  81. if (existing)
  82. existing.push(planet);
  83. else
  84. planetsByCX.set(wh, [planet]);
  85. }
  86. for (const [cx, planets] of planetsByCX)
  87. buys.append(...await calcForCX(username2, apiKey2, supplyForDays, planets, cx));
  88. }
  89. async function calcForCX(username2, apiKey2, supplyForDays, planets, cx) {
  90. const prices = await getPrices(cx);
  91. const avail = await warehouseInventory(username2, apiKey2, warehouseNames[cx]);
  92. const { bids, orders } = await getBids(username2, apiKey2, cx);
  93. const totalConsumption = new Map;
  94. for (const planet of planets) {
  95. for (const [ticker, consumption] of planet.netConsumption)
  96. totalConsumption.set(ticker, (totalConsumption.get(ticker) ?? 0) + consumption);
  97. for (const mat of planet.exporting) {
  98. const planetExport = planet.inventory.get(mat);
  99. if (planetExport)
  100. avail.set(mat, (avail.get(mat) ?? 0) + planetExport);
  101. }
  102. }
  103. const buy = new Map;
  104. for (const [mat, amount] of totalConsumption)
  105. if (amount > 0)
  106. buy.set(mat, (buy.get(mat) ?? 0) + amount * supplyForDays);
  107. const materials = [];
  108. for (const [mat, amount] of buy) {
  109. const remaining = Math.max(amount - (bids.get(mat) ?? 0) - (avail.get(mat) ?? 0), 0);
  110. const price = prices.get(mat);
  111. if (!price || price.Bid === null || price.Ask === null) {
  112. console.log(mat, "has no bid/ask");
  113. continue;
  114. }
  115. const spread = price.Ask - price.Bid;
  116. materials.push({
  117. ticker: mat,
  118. amount,
  119. bids: bids.get(mat) ?? 0,
  120. have: avail.get(mat) ?? 0,
  121. spread,
  122. savings: spread * remaining
  123. });
  124. }
  125. materials.sort((a, b) => b.savings - a.savings);
  126. const h2 = document.createElement("h2");
  127. h2.textContent = `${cx}/${warehouseNames[cx]}`;
  128. const span = document.createElement("span");
  129. span.textContent = planets.map((p) => p.name).join(", ");
  130. const table = document.createElement("table");
  131. table.innerHTML = `
  132. <thead>
  133. <tr>
  134. <th>mat</th>
  135. <th data-tooltip="needed to supply all planets">want</th>
  136. <th>bids</th>
  137. <th data-tooltip="in warehouse">have</th>
  138. <th data-tooltip="want - bids - have">buy</th>
  139. <th data-tooltip="ask - bid">spread</th>
  140. <th data-tooltip="buy × spread">savings</th>
  141. </tr>
  142. </thead>
  143. <tbody></tbody>`;
  144. const tbody = table.querySelector("tbody");
  145. const toBuy = {};
  146. const priceLimits = {};
  147. const format = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format;
  148. for (const m of materials) {
  149. const tr = document.createElement("tr");
  150. const buyAmount = Math.max(m.amount - m.bids - m.have, 0);
  151. tr.innerHTML = `
  152. <td>${m.ticker}</td>
  153. <td>${format(m.amount)}</td>
  154. <td>${format(m.bids)}</td>
  155. <td>${format(m.have)}</td>
  156. <td>${format(buyAmount)}</td>
  157. <td>${format(m.spread)}</td>
  158. <td>${format(m.savings)}</td>
  159. `;
  160. if (buyAmount > 0) {
  161. if (m.bids === 0)
  162. tr.children[2].classList.add("red");
  163. toBuy[m.ticker] = Math.round(buyAmount);
  164. const bid = prices.get(m.ticker).Bid;
  165. const epsilon = 10 ** (Math.floor(Math.log10(bid)) - 2);
  166. const limit = bid + 2 * epsilon;
  167. priceLimits[m.ticker] = limit;
  168. }
  169. tbody.appendChild(tr);
  170. }
  171. const xitSection = document.createElement("section");
  172. xitSection.classList.add("xit-act");
  173. xitSection.innerHTML = `
  174. <textarea readonly></textarea>
  175. <input type="button" value="copy">`;
  176. xitSection.querySelector("textarea").value = JSON.stringify({
  177. actions: [
  178. {
  179. name: "BuyItems",
  180. type: "CX Buy",
  181. group: "A1",
  182. exchange: cx,
  183. priceLimits,
  184. buyPartial: true,
  185. allowUnfilled: true,
  186. useCXInv: false
  187. }
  188. ],
  189. global: { name: `${cx} buy orders for ${supplyForDays} days` },
  190. groups: [{ type: "Manual", name: "A1", materials: toBuy }]
  191. });
  192. orders.sort((a, b) => b.Limit * b.Amount - a.Limit * a.Amount);
  193. for (const order of orders) {
  194. const deposit = order.Limit * order.Amount;
  195. console.log(`${order.MaterialTicker.padEnd(4)} ${deposit}
  196. `);
  197. }
  198. return [h2, span, table, xitSection];
  199. }
  200. async function getPrices(cx) {
  201. const rawPrices = await cachedFetchJSON("https://refined-prun.github.io/refined-prices/all.json");
  202. const prices = new Map;
  203. for (const p of rawPrices)
  204. if (p.ExchangeCode === cx)
  205. prices.set(p.MaterialTicker, p);
  206. return prices;
  207. }
  208. async function getPlanets(username2, apiKey2) {
  209. const fioBurns = await cachedFetchJSON("https://rest.fnar.net/fioweb/burn/user/" + username2, { headers: { Authorization: apiKey2 } });
  210. const planets = fioBurns.map((burn) => new Planet(burn));
  211. return planets;
  212. }
  213. async function warehouseInventory(username2, apiKey2, whName) {
  214. const warehouses = await cachedFetchJSON("https://rest.fnar.net/sites/warehouses/" + username2, { headers: { Authorization: apiKey2 } });
  215. const inventory = new Map;
  216. for (const warehouse of warehouses)
  217. if (warehouse.LocationNaturalId === whName) {
  218. const storage = await cachedFetchJSON(`https://rest.fnar.net/storage/${username2}/${warehouse.StoreId}`, { headers: { Authorization: apiKey2 } });
  219. for (const item of storage.StorageItems)
  220. inventory.set(item.MaterialTicker, item.MaterialAmount);
  221. break;
  222. }
  223. return inventory;
  224. }
  225. async function getBids(username2, apiKey2, cx) {
  226. const allOrders = await cachedFetchJSON("https://rest.fnar.net/cxos/" + username2, { headers: { Authorization: apiKey2 } });
  227. const orders = allOrders.filter((order) => order.OrderType === "BUYING" && order.Status !== "FILLED" && order.ExchangeCode === cx);
  228. const bids = new Map;
  229. for (const order of orders)
  230. bids.set(order.MaterialTicker, (bids.get(order.MaterialTicker) ?? 0) + order.Amount);
  231. return { bids, orders };
  232. }
  233. class Planet {
  234. id;
  235. name;
  236. inventory;
  237. netConsumption;
  238. exporting;
  239. constructor(fioBurn) {
  240. this.id = fioBurn.PlanetId;
  241. this.name = fioBurn.PlanetName || fioBurn.PlanetNaturalId;
  242. this.inventory = new Map;
  243. for (const item of fioBurn.Inventory)
  244. this.inventory.set(item.MaterialTicker, item.MaterialAmount);
  245. this.netConsumption = new Map;
  246. for (const item of fioBurn.OrderProduction)
  247. this.netConsumption.set(item.MaterialTicker, -item.DailyAmount);
  248. for (const c of [...fioBurn.OrderConsumption, ...fioBurn.WorkforceConsumption]) {
  249. this.netConsumption.set(c.MaterialTicker, (this.netConsumption.get(c.MaterialTicker) ?? 0) + c.DailyAmount);
  250. }
  251. this.exporting = new Set;
  252. for (const item of fioBurn.OrderProduction)
  253. if ((this.netConsumption.get(item.MaterialTicker) ?? 0) < 0)
  254. this.exporting.add(item.MaterialTicker);
  255. }
  256. }
  257. //# debugId=D0751901C98C88F664756E2164756E21