|
@@ -57,8 +57,6 @@ let opexMetric: MetricType = (localStorage.getItem('roi-opex-metric') as MetricT
|
|
|
let revenueMetric: MetricType = (localStorage.getItem('roi-revenue-metric') as MetricType) || 'vwap';
|
|
let revenueMetric: MetricType = (localStorage.getItem('roi-revenue-metric') as MetricType) || 'vwap';
|
|
|
let includeShips: boolean = localStorage.getItem('roi-include-ships') === 'true';
|
|
let includeShips: boolean = localStorage.getItem('roi-include-ships') === 'true';
|
|
|
|
|
|
|
|
-// EXTREME DETAIL: Added state tracking for the negative profit toggle.
|
|
|
|
|
-// It defaults to 'true' (showing negatives) to preserve previous user experience until toggled.
|
|
|
|
|
let showNegativeProfit: boolean = localStorage.getItem('roi-show-negative') !== 'false';
|
|
let showNegativeProfit: boolean = localStorage.getItem('roi-show-negative') !== 'false';
|
|
|
let workingCapitalDays: number = parseInt(localStorage.getItem('roi-working-capital') || '3', 10);
|
|
let workingCapitalDays: number = parseInt(localStorage.getItem('roi-working-capital') || '3', 10);
|
|
|
let targetPermit: number = parseInt(localStorage.getItem('roi-target-permit') || '2', 10);
|
|
let targetPermit: number = parseInt(localStorage.getItem('roi-target-permit') || '2', 10);
|
|
@@ -152,9 +150,10 @@ async function render() {
|
|
|
th.style.cursor = 'pointer';
|
|
th.style.cursor = 'pointer';
|
|
|
th.title = '';
|
|
th.title = '';
|
|
|
|
|
|
|
|
|
|
+ // EXTREME DETAIL: Updated the tooltip legend to explain that 100.0% is universally good.
|
|
|
if (keys[i] === 'profit_per_base') {
|
|
if (keys[i] === 'profit_per_base') {
|
|
|
th.textContent = 'Profit/Base';
|
|
th.textContent = 'Profit/Base';
|
|
|
- th.dataset.tooltip = 'Click to sort.\n\nDaily profit scaled to a full 500-area planetary base.\n(Percentiles rank 0.00 as lowest numerical value to 1.00 as highest)';
|
|
|
|
|
|
|
+ th.dataset.tooltip = 'Click to sort.\n\nDaily profit scaled to a full 500-area planetary base.\n(Percentiles rank 100.0% as the most desirable outcome, i.e., highest profit or lowest cost.)';
|
|
|
} else if (keys[i] === 'capex_val') {
|
|
} else if (keys[i] === 'capex_val') {
|
|
|
th.textContent = 'CapEx/Base';
|
|
th.textContent = 'CapEx/Base';
|
|
|
th.dataset.tooltip = 'Click to sort.\n\nTotal capital expenditure scaled to a full 500-area planetary base.\nIncludes base construction, working capital (days of OpEx), optional HQ Upgrade materials for the target permit, and optional Ship CapEx.';
|
|
th.dataset.tooltip = 'Click to sort.\n\nTotal capital expenditure scaled to a full 500-area planetary base.\nIncludes base construction, working capital (days of OpEx), optional HQ Upgrade materials for the target permit, and optional Ship CapEx.';
|
|
@@ -219,8 +218,6 @@ async function render() {
|
|
|
return true;
|
|
return true;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // EXTREME DETAIL: Notice that this is initialized as 'let' instead of 'const'.
|
|
|
|
|
- // This allows us to re-assign the array post-mapping if the negative profit toggle is active.
|
|
|
|
|
let profitsWithMetrics: ProfitWithMetrics[] = filteredProfits.map(p => {
|
|
let profitsWithMetrics: ProfitWithMetrics[] = filteredProfits.map(p => {
|
|
|
const bases = p.area / 500;
|
|
const bases = p.area / 500;
|
|
|
const opex_val = p.opex[opexMetric] / bases;
|
|
const opex_val = p.opex[opexMetric] / bases;
|
|
@@ -247,10 +244,6 @@ async function render() {
|
|
|
return { ...p, capex_val, opex_val, revenue_val, profit_per_base, break_even, hq_capex };
|
|
return { ...p, capex_val, opex_val, revenue_val, profit_per_base, break_even, hq_capex };
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // EXTREME DETAIL: We filter the array again AFTER mapping the dynamic pricing logic.
|
|
|
|
|
- // This ensures we are testing the user's specific VWAP/Bid/Ask permutation, and doing
|
|
|
|
|
- // this before the percentile arrays are generated guarantees money-losing bases are entirely
|
|
|
|
|
- // excluded from the active percentile population.
|
|
|
|
|
if (!showNegativeProfit) {
|
|
if (!showNegativeProfit) {
|
|
|
profitsWithMetrics = profitsWithMetrics.filter(p => p.profit_per_base > 0);
|
|
profitsWithMetrics = profitsWithMetrics.filter(p => p.profit_per_base > 0);
|
|
|
}
|
|
}
|
|
@@ -280,12 +273,15 @@ async function render() {
|
|
|
for (const p of profitsWithMetrics) {
|
|
for (const p of profitsWithMetrics) {
|
|
|
const tr = document.createElement('tr');
|
|
const tr = document.createElement('tr');
|
|
|
|
|
|
|
|
- const pctProfit = getPercentile(p.profit_per_base, arrProfit);
|
|
|
|
|
- const pctBreak = getPercentile(p.break_even, arrBreak);
|
|
|
|
|
- const pctCapex = getPercentile(p.capex_val, arrCapex);
|
|
|
|
|
- const pctOpex = getPercentile(p.opex_val, arrOpex);
|
|
|
|
|
- const pctLog = getPercentile(p.normalized_logistics_per_base, arrLog);
|
|
|
|
|
- const pctCap = getPercentile(p.market_capacity_base, arrCap);
|
|
|
|
|
|
|
+ // EXTREME DETAIL: We explicitly route each column to its correct `invert` state.
|
|
|
|
|
+ // Profit and Market Cap use `false` (highest numerical value = 100%).
|
|
|
|
|
+ // Break Even, CapEx, OpEx, and Logistics use `true` (lowest numerical value = 100%).
|
|
|
|
|
+ const pctProfit = getPercentile(p.profit_per_base, arrProfit, false);
|
|
|
|
|
+ const pctBreak = getPercentile(p.break_even, arrBreak, true);
|
|
|
|
|
+ const pctCapex = getPercentile(p.capex_val, arrCapex, true);
|
|
|
|
|
+ const pctOpex = getPercentile(p.opex_val, arrOpex, true);
|
|
|
|
|
+ const pctLog = getPercentile(p.normalized_logistics_per_base, arrLog, true);
|
|
|
|
|
+ const pctCap = getPercentile(p.market_capacity_base, arrCap, false);
|
|
|
|
|
|
|
|
tr.innerHTML = `
|
|
tr.innerHTML = `
|
|
|
<td>${p.outputs.map(o => o.ticker).join(', ')}</td>
|
|
<td>${p.outputs.map(o => o.ticker).join(', ')}</td>
|
|
@@ -331,14 +327,22 @@ async function render() {
|
|
|
saveState();
|
|
saveState();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function getPercentile(val: number, sortedArr: number[]): string {
|
|
|
|
|
- if (sortedArr.length < 2) return "1.00";
|
|
|
|
|
|
|
+// EXTREME DETAIL: Added 'invert' boolean to flip logic for cost metrics.
|
|
|
|
|
+// Returns a heavily formatted string e.g., '99.0%'.
|
|
|
|
|
+function getPercentile(val: number, sortedArr: number[], invert: boolean = false): string {
|
|
|
|
|
+ if (sortedArr.length < 2) return "100.0%";
|
|
|
let less = 0;
|
|
let less = 0;
|
|
|
for (let i = 0; i < sortedArr.length; i++) {
|
|
for (let i = 0; i < sortedArr.length; i++) {
|
|
|
if (sortedArr[i] < val) less++;
|
|
if (sortedArr[i] < val) less++;
|
|
|
else break;
|
|
else break;
|
|
|
}
|
|
}
|
|
|
- return (less / (sortedArr.length - 1)).toFixed(2);
|
|
|
|
|
|
|
+
|
|
|
|
|
+ let decimal = less / (sortedArr.length - 1);
|
|
|
|
|
+ if (invert) {
|
|
|
|
|
+ decimal = 1.0 - decimal;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return (decimal * 100).toFixed(1) + "%";
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function saveState() {
|
|
function saveState() {
|