roi.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. // ts/popover.ts
  2. function setupPopover() {
  3. const main = document.querySelector("main");
  4. const popover = document.querySelector("#popover");
  5. main.addEventListener("mouseover", (event) => {
  6. const target = event.target;
  7. if (target.dataset.tooltip) {
  8. popover.textContent = target.dataset.tooltip;
  9. const rect = target.getBoundingClientRect();
  10. popover.style.left = `${rect.left}px`;
  11. popover.style.top = `${rect.bottom}px`;
  12. popover.showPopover();
  13. }
  14. });
  15. main.addEventListener("mouseout", (event) => {
  16. const target = event.target;
  17. if (target.dataset.tooltip)
  18. popover.hidePopover();
  19. });
  20. }
  21. // ts/roi.ts
  22. var roiCache = {};
  23. async function getROI(cx) {
  24. const response = await fetch(`/roi_${cx.toLowerCase()}.json`);
  25. const lastModified = new Date(response.headers.get("last-modified"));
  26. const profits = await response.json();
  27. return { lastModified, profits };
  28. }
  29. var lowVolume = document.querySelector("input#low-volume");
  30. var cxSelect = document.querySelector("select#cx");
  31. var expertise = {
  32. AGRICULTURE: "agri",
  33. CHEMISTRY: "chem",
  34. CONSTRUCTION: "const",
  35. ELECTRONICS: "elec",
  36. FOOD_INDUSTRIES: "food ind",
  37. FUEL_REFINING: "fuel",
  38. MANUFACTURING: "mfg",
  39. METALLURGY: "metal",
  40. RESOURCE_EXTRACTION: "res ext"
  41. };
  42. var expertiseSelect = document.querySelector("select#expertise");
  43. for (const key of Object.keys(expertise)) {
  44. const option = document.createElement("option");
  45. option.value = key;
  46. option.textContent = key.replace("_", " ").toLowerCase();
  47. expertiseSelect.appendChild(option);
  48. }
  49. var buildingSelect = document.querySelector("select#building");
  50. var formatDecimal = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2, maximumSignificantDigits: 6, roundingPriority: "lessPrecision" }).format;
  51. var formatWhole = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format;
  52. var currentSortKey = "break_even";
  53. var currentSortAsc = true;
  54. var headersInitialized = false;
  55. var metricControlsInitialized = false;
  56. var capexMetric = "vwap";
  57. var opexMetric = "vwap";
  58. var revenueMetric = "vwap";
  59. async function render() {
  60. const tbody = document.querySelector("tbody");
  61. tbody.innerHTML = "";
  62. const cx = cxSelect.value;
  63. if (!roiCache[cx])
  64. roiCache[cx] = getROI(cx);
  65. const { lastModified, profits } = await roiCache[cx];
  66. if (!metricControlsInitialized) {
  67. const controls = document.createElement("div");
  68. controls.style.marginBottom = "15px";
  69. controls.innerHTML = `
  70. <label style="margin-right: 15px;">CapEx Price:
  71. <select id="capex-metric"><option value="vwap">VWAP</option><option value="bid">Bid</option><option value="ask">Ask</option></select>
  72. </label>
  73. <label style="margin-right: 15px;">OpEx Price:
  74. <select id="opex-metric"><option value="vwap">VWAP</option><option value="bid">Bid</option><option value="ask">Ask</option></select>
  75. </label>
  76. <label>Revenue (Outputs) Price:
  77. <select id="revenue-metric"><option value="vwap">VWAP</option><option value="bid">Bid</option><option value="ask">Ask</option></select>
  78. </label>
  79. `;
  80. const table = document.querySelector("table");
  81. if (table)
  82. table.parentNode?.insertBefore(controls, table);
  83. document.getElementById("capex-metric").addEventListener("change", (e) => {
  84. capexMetric = e.target.value;
  85. render();
  86. });
  87. document.getElementById("opex-metric").addEventListener("change", (e) => {
  88. opexMetric = e.target.value;
  89. render();
  90. });
  91. document.getElementById("revenue-metric").addEventListener("change", (e) => {
  92. revenueMetric = e.target.value;
  93. render();
  94. });
  95. metricControlsInitialized = true;
  96. }
  97. if (!headersInitialized) {
  98. const ths = document.querySelectorAll("th");
  99. const keys = [
  100. "outputs",
  101. "expertise",
  102. "profit_per_area",
  103. "break_even",
  104. "capex_val",
  105. "opex_val",
  106. "logistics_per_area",
  107. "market_capacity_area"
  108. ];
  109. ths.forEach((th, i) => {
  110. if (keys[i]) {
  111. th.style.cursor = "pointer";
  112. th.title = "Click to sort";
  113. th.addEventListener("click", () => {
  114. if (currentSortKey === keys[i]) {
  115. currentSortAsc = !currentSortAsc;
  116. } else {
  117. currentSortKey = keys[i];
  118. currentSortAsc = keys[i] === "break_even" ? true : false;
  119. }
  120. render();
  121. });
  122. }
  123. });
  124. headersInitialized = true;
  125. }
  126. const buildingTickers = new Set(profits.map((p) => p.building));
  127. const buildings = Array.from(buildingTickers).map((building) => ({ ticker: building, expertise: profits.find((p) => p.building === building).expertise })).sort((a, b) => a.ticker.localeCompare(b.ticker));
  128. let selectedBuilding = buildingSelect.value;
  129. let buildingFound = false;
  130. buildingSelect.innerHTML = '<option value="">(all)</option>';
  131. for (const building of buildings)
  132. if (expertiseSelect.value === "" || expertiseSelect.value === building.expertise) {
  133. const option = document.createElement("option");
  134. option.value = building.ticker;
  135. option.textContent = building.ticker;
  136. if (building.ticker === selectedBuilding) {
  137. buildingFound = true;
  138. option.selected = true;
  139. }
  140. buildingSelect.appendChild(option);
  141. }
  142. if (!buildingFound)
  143. selectedBuilding = "";
  144. const filteredProfits = profits.filter((p) => {
  145. const volumeRatio = p.output_per_day / p.average_traded_7d;
  146. if (!lowVolume.checked && volumeRatio > 0.05)
  147. return false;
  148. if (expertiseSelect.value !== "" && p.expertise !== expertiseSelect.value)
  149. return false;
  150. if (selectedBuilding !== "" && p.building !== selectedBuilding)
  151. return false;
  152. return true;
  153. });
  154. const profitsWithMetrics = filteredProfits.map((p) => {
  155. const capex_val = p.capex[capexMetric];
  156. const opex_val = p.opex[opexMetric];
  157. const revenue_val = p.revenue[revenueMetric];
  158. const profit_per_day = revenue_val - opex_val;
  159. const profit_per_area = profit_per_day / p.area;
  160. const break_even = profit_per_day > 0 ? (capex_val + 3 * opex_val) / profit_per_day : Infinity;
  161. return { ...p, capex_val, opex_val, revenue_val, profit_per_day, profit_per_area, break_even };
  162. });
  163. profitsWithMetrics.sort((a, b) => {
  164. let valA = a[currentSortKey];
  165. let valB = b[currentSortKey];
  166. if (currentSortKey === "outputs") {
  167. valA = a.outputs.map((o) => o.ticker).join(", ");
  168. valB = b.outputs.map((o) => o.ticker).join(", ");
  169. }
  170. if (valA < valB)
  171. return currentSortAsc ? -1 : 1;
  172. if (valA > valB)
  173. return currentSortAsc ? 1 : -1;
  174. return 0;
  175. });
  176. for (const p of profitsWithMetrics) {
  177. const volumeRatio = p.output_per_day / p.average_traded_7d;
  178. const tr = document.createElement("tr");
  179. tr.innerHTML = `
  180. <td>${p.outputs.map((o) => o.ticker).join(", ")}</td>
  181. <td>${expertise[p.expertise]}</td>
  182. <td style="color: ${color(p.profit_per_area, 0, 300)}">${formatDecimal(p.profit_per_area)}</td>
  183. <td><span style="color: ${color(p.break_even, 30, 3)}">${formatDecimal(p.break_even)}</span>d</td>
  184. <td style="color: ${color(p.capex_val, 300000, 40000)}">${formatWhole(p.capex_val)}</td>
  185. <td style="color: ${color(p.opex_val, 40000, 1000)}">${formatWhole(p.opex_val)}</td>
  186. <td style="color: ${color(p.logistics_per_area, 2, 0.2)}">${formatDecimal(p.logistics_per_area)}</td>
  187. <td style="color: ${color(p.market_capacity_area, 20, 500)}">${formatWhole(p.market_capacity_area)}</td>
  188. `;
  189. const output = tr.querySelector("td");
  190. output.dataset.tooltip = p.recipe;
  191. const profitCell = tr.querySelectorAll("td")[2];
  192. profitCell.dataset.tooltip = formatMatPrices(p.outputs, revenueMetric, p.runs_per_day) + `
  193. ` + formatMatPrices(p.input_costs, opexMetric, p.runs_per_day) + `
  194. ` + `(${formatWhole(p.revenue_val)} - ${formatWhole(p.opex_val)}) = ${formatDecimal(p.profit_per_day)}
  195. ` + `${formatDecimal(p.profit_per_day)} / ${formatWhole(p.area)} area = ${formatDecimal(p.profit_per_area)}`;
  196. const marketCell = tr.querySelectorAll("td")[7];
  197. marketCell.dataset.tooltip = `Market Capacity: ${formatWhole(p.average_traded_7d)} traded/day ÷ ${formatDecimal(p.output_per_day / p.area)} produced/day/area = ${formatWhole(p.market_capacity_area)} equivalent areas`;
  198. tbody.appendChild(tr);
  199. }
  200. }
  201. function color(n, low, high) {
  202. const scale = Math.min(Math.max((n - low) / (high - low), 0), 1);
  203. return `color-mix(in xyz, #0aa ${scale * 100}%, #f80)`;
  204. }
  205. function formatMatPrices(matPrices, metric, runs_per_day) {
  206. return matPrices.map(({ ticker, amount, vwap_7d, bid, ask }) => {
  207. const val = metric === "vwap" ? vwap_7d : metric === "bid" ? bid ?? vwap_7d : ask ?? vwap_7d;
  208. const daily_amount = amount * runs_per_day;
  209. return `${ticker}: ${formatDecimal(daily_amount)} × ${formatDecimal(val)} = ${formatWhole(daily_amount * val)}`;
  210. }).join(`
  211. `);
  212. }
  213. setupPopover();
  214. lowVolume.addEventListener("change", render);
  215. cxSelect.addEventListener("change", render);
  216. expertiseSelect.addEventListener("change", render);
  217. buildingSelect.addEventListener("change", render);
  218. render();
  219. //# debugId=96EE1F33EAAE56CC64756E2164756E21