diff --git a/.gitignore b/.gitignore index 5ef6a52..7263d49 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,12 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Scenarios +/scenarios + +## Tests related to specific LBP clients +project-scenarios.test.js + +## Spreadsheet outputs +/out \ No newline at end of file diff --git a/components/lbp-simulator/simulator/DemandPressureConfig.tsx b/components/lbp-simulator/simulator/DemandPressureConfig.tsx index 9426a42..68d3695 100644 --- a/components/lbp-simulator/simulator/DemandPressureConfig.tsx +++ b/components/lbp-simulator/simulator/DemandPressureConfig.tsx @@ -40,6 +40,39 @@ import { useDebounce } from "@/lib/useDebounce"; import { useShallow } from "zustand/react/shallow"; import { GiBull } from "react-icons/gi"; import { GiBearFace } from "react-icons/gi"; +import { formatNumber } from "@/lib/utils"; + +const MAGNITUDE_BASES = [10_000, 100_000, 1_000_000] as const; + +function parseNumberInput(raw: string) { + const cleaned = raw.replace(/,/g, "").trim(); + if (cleaned === "") return null; + const n = Number(cleaned); + if (!Number.isFinite(n)) return null; + return n; +} + +function pickMagnitudeBaseForTarget(target: number) { + // Prefer multipliers in a "nice" range so the UI stays readable. + let best = 100_000 as (typeof MAGNITUDE_BASES)[number]; + let bestScore = Number.POSITIVE_INFINITY; + + for (const base of MAGNITUDE_BASES) { + const mult = target / base; + const penaltyLow = mult < 0.1 ? 10 + (0.1 - mult) * 50 : 0; + const penaltyHigh = mult > 20 ? 10 + (mult - 20) : 0; + const score = + penaltyLow + + penaltyHigh + + Math.abs(Math.log10(Math.max(1e-9, mult))); + if (score < bestScore) { + bestScore = score; + best = base; + } + } + + return best; +} function DemandPressureConfigComponent() { const { demandPressureConfig, updateDemandPressureConfig, config } = @@ -54,12 +87,25 @@ function DemandPressureConfigComponent() { // Local state for immediate UI updates const [localConfig, setLocalConfig] = useState(demandPressureConfig); + const [endCumulativeInput, setEndCumulativeInput] = useState(""); + const [isEditingEndCumulative, setIsEditingEndCumulative] = useState(false); // Update local state when store config changes (e.g., reset) useEffect(() => { setLocalConfig(demandPressureConfig); }, [demandPressureConfig]); + const endScale = localConfig.preset === "bearish" ? 0.35 : 1.0; + const endCumulativeValue = + localConfig.magnitudeBase * localConfig.multiplier * endScale; + + // Keep the "single number" input in sync with underlying magnitudeBase * multiplier (and preset scaling), + // but don't fight the user while they're typing. + useEffect(() => { + if (isEditingEndCumulative) return; + setEndCumulativeInput(formatNumber(Math.round(endCumulativeValue))); + }, [endCumulativeValue, isEditingEndCumulative]); + // Debounce the local config before updating the store const debouncedConfig = useDebounce(localConfig, 500); @@ -195,7 +241,22 @@ function DemandPressureConfigComponent() {