Forráskód Böngészése

Add_Negative_Profit_Toggle

Implemented a UI toggle to filter out negative-profit recipes post-derivation.

        PURPOSE:
        To immediately declutter the table by vanishing mathematically unviable base builds. By establishing this filter *after* the dynamic metric evaluation (VWAP/Bid/Ask), but *before* the percentile arrays are generated, the mathematical baseline for percentiles remains pure and context-accurate.

        IMPLEMENTATION DETAILS:
        - `ts/roi.ts`: Added `<input type="checkbox" id="show-negative">` to the DOM header block, bound to the `showNegativeProfit` state variable (defaulting to true) and synced with `localStorage`.
        - `ts/roi.ts`: Changed the instantiation of `profitsWithMetrics` from `const` to `let`.
        - `ts/roi.ts`: Injected an `if (!showNegativeProfit)` block immediately following the `.map()` derivation block. This explicitly overwrites the array with a strict `> 0` profit filter.
        - Because this structural wipe occurs prior to `arrProfit`, `arrBreak`, etc. being populated, all money-losing rows are flawlessly excluded from the percentile generation logic.
Thomas Knott 2 hete
szülő
commit
e5ca96babe
1 módosított fájl, 26 hozzáadás és 21 törlés
  1. 26 21
      ts/roi.ts

+ 26 - 21
ts/roi.ts

@@ -44,8 +44,6 @@ if (localStorage.getItem('roi-low-volume')) lowVolume.checked = localStorage.get
 
 let savedBuilding = localStorage.getItem('roi-building') || '';
 
-// EXTREME DETAIL: Legacy cache migration. If the user had the old raw 'logistics_per_base' stored
-// as their active sort, we organically migrate them to the new normalized property.
 let storedSortKey = localStorage.getItem('roi-sort-key') as any;
 if (storedSortKey === 'logistics_per_base') storedSortKey = 'normalized_logistics_per_base';
 let currentSortKey: keyof ProfitWithMetrics | 'outputs' = storedSortKey || 'break_even';
@@ -58,6 +56,10 @@ let capexMetric: MetricType = (localStorage.getItem('roi-capex-metric') as Metri
 let opexMetric: MetricType = (localStorage.getItem('roi-opex-metric') as MetricType) || 'vwap';
 let revenueMetric: MetricType = (localStorage.getItem('roi-revenue-metric') as MetricType) || 'vwap';
 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 workingCapitalDays: number = parseInt(localStorage.getItem('roi-working-capital') || '3', 10);
 let targetPermit: number = parseInt(localStorage.getItem('roi-target-permit') || '2', 10);
 
@@ -86,6 +88,9 @@ async function render() {
 			<label style="margin-right: 15px;">
 				<input type="checkbox" id="include-ships"> Include Ship CapEx
 			</label>
+			<label style="margin-right: 15px;">
+				<input type="checkbox" id="show-negative"> Show Negative Profit
+			</label>
 			<label style="margin-right: 15px;">
 				Days OpEx: <input type="number" id="working-capital" min="0" step="1" style="width: 50px;">
 			</label>
@@ -100,6 +105,7 @@ async function render() {
 		(document.getElementById('opex-metric') as HTMLSelectElement).value = opexMetric;
 		(document.getElementById('revenue-metric') as HTMLSelectElement).value = revenueMetric;
 		(document.getElementById('include-ships') as HTMLInputElement).checked = includeShips;
+		(document.getElementById('show-negative') as HTMLInputElement).checked = showNegativeProfit;
 		(document.getElementById('working-capital') as HTMLInputElement).value = workingCapitalDays.toString();
 		(document.getElementById('target-permit') as HTMLInputElement).value = targetPermit.toString();
 
@@ -119,6 +125,10 @@ async function render() {
 			includeShips = (e.target as HTMLInputElement).checked;
 			render();
 		});
+		document.getElementById('show-negative')!.addEventListener('change', (e) => {
+			showNegativeProfit = (e.target as HTMLInputElement).checked;
+			render();
+		});
 		document.getElementById('working-capital')!.addEventListener('change', (e) => {
 			workingCapitalDays = parseInt((e.target as HTMLInputElement).value, 10);
 			render();
@@ -132,9 +142,6 @@ async function render() {
 
 	if (!headersInitialized) {
 		const ths = document.querySelectorAll('th');
-		// EXTREME DETAIL: We swapped `logistics_per_base` for `normalized_logistics_per_base`.
-		// Clicking the Logistics header now intrinsically triggers a sort utilizing the ship fraction,
-		// ensuring that 1000m³ perfectly balances against 3000t.
 		const keys: (keyof ProfitWithMetrics | 'outputs')[] = [
 			'outputs', 'expertise', 'profit_per_base', 'break_even', 
 			'capex_val', 'opex_val', 'normalized_logistics_per_base', 'market_capacity_base'
@@ -204,9 +211,6 @@ async function render() {
 	
 	savedBuilding = ''; 
 
-	// EXTREME DETAIL: Because the percentile derivations evaluate the arrays *after* the filter executes,
-	// any row removed by the "low volume" constraint is automatically excluded from the 
-	// percentile population math, ensuring your rankings are entirely context-accurate.
 	const filteredProfits = profits.filter(p => {
 		const volumeRatio = p.output_per_day / p.average_traded_7d;
 		if (!lowVolume.checked && volumeRatio > 0.05) return false;
@@ -215,7 +219,9 @@ async function render() {
 		return true;
 	});
 
-	const profitsWithMetrics: ProfitWithMetrics[] = filteredProfits.map(p => {
+	// 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 => {
 		const bases = p.area / 500;
 		const opex_val = p.opex[opexMetric] / bases;
 		const revenue_val = p.revenue[revenueMetric] / bases;
@@ -241,8 +247,14 @@ async function render() {
 		return { ...p, capex_val, opex_val, revenue_val, profit_per_base, break_even, hq_capex };
 	});
 
-	// EXTREME DETAIL: We extract 6 sorted arrays of our active numeric values post-mapping.
-	// Using a robust numeric sorting function protects against `NaN` crashes when dealing with `Infinity`.
+	// 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) {
+		profitsWithMetrics = profitsWithMetrics.filter(p => p.profit_per_base > 0);
+	}
+
 	const numSort = (a: number, b: number) => (a < b ? -1 : a > b ? 1 : 0);
 	const arrProfit = profitsWithMetrics.map(p => p.profit_per_base).sort(numSort);
 	const arrBreak = profitsWithMetrics.map(p => p.break_even).sort(numSort);
@@ -268,7 +280,6 @@ async function render() {
 	for (const p of profitsWithMetrics) {
 		const tr = document.createElement('tr');
 		
-		// Map the raw value to its percentile rank within the active filtered column.
 		const pctProfit = getPercentile(p.profit_per_base, arrProfit);
 		const pctBreak = getPercentile(p.break_even, arrBreak);
 		const pctCapex = getPercentile(p.capex_val, arrCapex);
@@ -276,10 +287,6 @@ async function render() {
 		const pctLog = getPercentile(p.normalized_logistics_per_base, arrLog);
 		const pctCap = getPercentile(p.market_capacity_base, arrCap);
 		
-		// EXTREME DETAIL: We append the percentiles enclosed in a <small> tag.
-		// Reducing the opacity slightly creates visual hierarchy, allowing the 3-sig-fig
-		// metrics to remain the primary focal point of the table while the percentile acts as metadata.
-		// Note: The Logistics color mapping natively targets `normalized_logistics_per_base` (0.1 to 1.0 ship bounds).
 		tr.innerHTML = `
 			<td>${p.outputs.map(o => o.ticker).join(', ')}</td>
 			<td>${expertise[p.expertise]}</td>
@@ -324,15 +331,12 @@ async function render() {
 	saveState();
 }
 
-// EXTREME DETAIL: O(N) simple percentile ranker. 
-// Locates how many array items are strictly less than the target value.
-// Bounding it against `sortedArr.length - 1` gracefully maps the output from 0.00 to 1.00.
 function getPercentile(val: number, sortedArr: number[]): string {
 	if (sortedArr.length < 2) return "1.00";
 	let less = 0;
 	for (let i = 0; i < sortedArr.length; i++) {
 		if (sortedArr[i] < val) less++;
-		else break; // Break early because array is guaranteed pre-sorted
+		else break; 
 	}
 	return (less / (sortedArr.length - 1)).toFixed(2);
 }
@@ -348,6 +352,7 @@ function saveState() {
 	localStorage.setItem('roi-opex-metric', opexMetric);
 	localStorage.setItem('roi-revenue-metric', revenueMetric);
 	localStorage.setItem('roi-include-ships', includeShips.toString());
+	localStorage.setItem('roi-show-negative', showNegativeProfit.toString());
 	localStorage.setItem('roi-working-capital', workingCapitalDays.toString());
 	localStorage.setItem('roi-target-permit', targetPermit.toString());
 }
@@ -390,7 +395,7 @@ interface Profit {
 	input_costs: MatPrice[]
 	runs_per_day: number
 	logistics_per_base: number
-	normalized_logistics_per_base: number // Extracted from backend explicitly for sorting/percentiles
+	normalized_logistics_per_base: number 
 	logistics_bottleneck: string 
 	output_per_day: number
 	average_traded_7d: number