Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
347 changes: 304 additions & 43 deletions components/lbp-simulator/simulator/DemandPressureConfig.tsx

Large diffs are not rendered by default.

93 changes: 83 additions & 10 deletions components/lbp-simulator/simulator/SimulatorConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,16 @@ import {
import { useSimulatorStore } from "@/store/useSimulatorStore";
import { DemandPressureConfig } from "./DemandPressureConfig";
import { SellPressureConfig } from "./SellPressureConfig";
import { useState, useEffect, useTransition, memo, useCallback } from "react";
import {
useState,
useEffect,
useTransition,
memo,
useCallback,
useRef,
} from "react";
import { useDebounce } from "@/lib/useDebounce";
import { LBPConfig } from "@/lib/lbp-math";
import type { SellPressureConfig as SellPressureConfigType } from "@/lib/lbp-math";
import { useShallow } from "zustand/shallow";
import { TokenLogo } from "@/components/ui/TokenLogo";
import { formatNumber } from "@/lib/utils";
Expand All @@ -49,6 +56,7 @@ function SimulatorConfigComponent() {
simulationSpeed,
setSimulationSpeed,
updateSellPressureConfig,
sellPressureConfig,
} = useSimulatorStore(
useShallow((state) => ({
config: state.config,
Expand All @@ -60,6 +68,7 @@ function SimulatorConfigComponent() {
simulationSpeed: state.simulationSpeed,
setSimulationSpeed: state.setSimulationSpeed,
updateSellPressureConfig: state.updateSellPressureConfig,
sellPressureConfig: state.sellPressureConfig,
})),
);

Expand Down Expand Up @@ -93,7 +102,19 @@ function SimulatorConfigComponent() {
config.usdcBalanceIn,
);
const [pressureMode, setPressureMode] = useState<"buy-and-sell" | "buy-only">(
"buy-and-sell",
() => {
if (sellPressureConfig.preset === "loyal") {
return sellPressureConfig.loyalSoldPct <= 0
? "buy-only"
: "buy-and-sell";
}
return sellPressureConfig.greedySellPct <= 0
? "buy-only"
: "buy-and-sell";
},
);
const sellConfigBeforeBuyOnlyRef = useRef<SellPressureConfigType | null>(
null,
);

// Update local state when store config changes
Expand Down Expand Up @@ -282,7 +303,27 @@ function SimulatorConfigComponent() {
onValueChange={(value: "buy-and-sell" | "buy-only") => {
setPressureMode(value);
if (value === "buy-only") {
updateSellPressureConfig({ loyalSoldPct: 0 });
// Buy-only should disable sell pressure regardless of the current preset
// (e.g. if the user previously selected "greedy", that still sells).
if (sellConfigBeforeBuyOnlyRef.current == null) {
sellConfigBeforeBuyOnlyRef.current =
sellPressureConfig;
}
updateSellPressureConfig({
preset: "loyal",
loyalSoldPct: 0,
greedySellPct: 0,
});
// Clear prior Sales rows + prevent ticking against stale snapshots.
restartSimulation();
} else {
const prev = sellConfigBeforeBuyOnlyRef.current;
if (prev != null) {
sellConfigBeforeBuyOnlyRef.current = null;
updateSellPressureConfig(prev);
}
// New sell config means a new deterministic path; restart for a clean run.
restartSimulation();
}
}}
className="flex"
Expand Down Expand Up @@ -314,7 +355,9 @@ function SimulatorConfigComponent() {
: "opacity-0 pointer-events-none"
}`}
>
<SellPressureConfig />
{pressureMode === "buy-and-sell" ? (
<SellPressureConfig />
) : null}
</div>
</div>
</div>
Expand Down Expand Up @@ -401,7 +444,7 @@ function SimulatorConfigComponent() {
<Label>Start (Token / {config.collateralToken})</Label>
<div className="flex items-center gap-3">
<span className="text-sm font-mono w-10 shrink-0">
{config.tknWeightIn}%
{localTknWeightIn}%
</span>
<Slider
value={[localTknWeightIn]}
Expand All @@ -412,15 +455,30 @@ function SimulatorConfigComponent() {
className="flex-1"
/>
<span className="text-sm font-mono w-10 shrink-0 text-right">
{config.usdcWeightIn}%
{100 - localTknWeightIn}%
</span>
<Input
className="w-12 shrink-0 text-right p-0"
type="number"
min={1}
max={99}
step={1}
value={localTknWeightIn}
onChange={(e) => {
const next = Number(e.target.value);
if (!Number.isFinite(next)) return;
handleWeightChange(
Math.max(1, Math.min(99, Math.round(next))),
);
}}
/>
</div>
</div>
<div className="space-y-2">
<Label>End (Token / {config.collateralToken})</Label>
<div className="flex items-center gap-3">
<span className="text-sm font-mono w-10 shrink-0">
{config.tknWeightOut}%
{localTknWeightOut}%
</span>
<Slider
value={[localTknWeightOut]}
Expand All @@ -431,8 +489,23 @@ function SimulatorConfigComponent() {
className="flex-1"
/>
<span className="text-sm font-mono w-10 shrink-0 text-right">
{config.usdcWeightOut}%
{100 - localTknWeightOut}%
</span>
<Input
className="w-12 shrink-0 text-right p-0"
type="number"
min={1}
max={99}
step={1}
value={localTknWeightOut}
onChange={(e) => {
const next = Number(e.target.value);
if (!Number.isFinite(next)) return;
handleEndWeightChange(
Math.max(1, Math.min(99, Math.round(next))),
);
}}
/>
</div>
</div>
</div>
Expand Down Expand Up @@ -481,7 +554,7 @@ function SimulatorConfigComponent() {
<SelectValue placeholder="Select swap fee" />
</SelectTrigger>
<SelectContent>
{[1, 2, 3, 4, 5, 6, 7 , 8, 9, 10].map((fee) => (
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((fee) => (
<SelectItem key={fee} value={String(fee)}>
{fee}%
</SelectItem>
Expand Down
51 changes: 44 additions & 7 deletions components/lbp-simulator/simulator/SimulatorMain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,34 @@
import { useSimulatorStore } from "@/store/useSimulatorStore";
import { useShallow } from "zustand/react/shallow";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useEffect, memo } from "react";
import { useEffect, useMemo, useState, memo } from "react";
import { useSimulationWorker } from "@/lib/hooks/useSimulationWorker";
import { SimulatorChartArea } from "./SimulatorChartArea";

function SalesSwapsCountBadge({ isVisible }: { isVisible: boolean }) {
const swapCount = useSimulatorStore((state) => state.swaps.length);

const label = useMemo(() => {
if (!isVisible) return "";
return `Swaps: ${swapCount.toLocaleString()}`;
}, [isVisible, swapCount]);

if (!isVisible) return null;

return (
<div
className="ml-auto self-center flex items-center whitespace-nowrap rounded-md border border-emerald-200 dark:border-emerald-900/50 bg-emerald-100/70 dark:bg-emerald-900/20 px-2 py-1 text-[11px] text-emerald-900 dark:text-emerald-100"
title="Number of swap rows recorded in the Sales table (community per-step + user actions)."
aria-label={label}
>
<span className="font-semibold">Swaps:</span>
<span className="ml-1 font-mono tabular-nums">
{swapCount.toLocaleString()}
</span>
</div>
);
}

/**
* Chart area shell; subscribes only to static/slow-changing state (config, etc.).
* SwapForm is rendered as a sibling in Simulator so it never re-renders when this re-renders.
Expand All @@ -22,6 +46,10 @@ function SimulatorMainComponent() {
})),
);

const [activeTab, setActiveTab] = useState<
"chart" | "swaps" | "demand" | "weights"
>("chart");

const { snapshots: workerSnapshots } = useSimulationWorker(
config,
demandPressureConfig,
Expand All @@ -38,7 +66,13 @@ function SimulatorMainComponent() {
}, [workerSnapshots, setBaseSnapshots]);

return (
<Tabs defaultValue="chart" className="w-full">
<Tabs
value={activeTab}
onValueChange={(v) =>
setActiveTab(v as "chart" | "swaps" | "demand" | "weights")
}
className="w-full"
>
<div className="flex items-center justify-between mb-2">
<TabsList className="bg-transparent p-0 justify-start h-auto border-b w-full rounded-none">
<TabsTrigger
Expand Down Expand Up @@ -69,7 +103,7 @@ function SimulatorMainComponent() {
</div>

<div className="border rounded-md bg-background/50 p-4 relative h-[560px]">
<div className="mb-3 p-2 bg-emerald-50 dark:bg-emerald-950/20 border border-emerald-200 dark:border-emerald-900/50 rounded-md flex items-start gap-2">
<div className="mb-3 p-2 bg-emerald-50 dark:bg-emerald-950/20 border border-emerald-200 dark:border-emerald-900/50 rounded-md flex items-center gap-2">
<svg
className="h-4 w-4 text-emerald-600 dark:text-emerald-400 mt-0.5 flex-shrink-0"
fill="none"
Expand All @@ -83,11 +117,14 @@ function SimulatorMainComponent() {
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<p className="text-xs text-emerald-800 dark:text-emerald-200">
<span className="font-semibold">Dynamic price:</span> Decays with
time, rises with demand. Dotted lines show potential price paths
based on different demand scenarios.
<p className="text-xs flex items-center gap-1 text-emerald-800 dark:text-emerald-200 flex-1">
<span className="font-semibold">Dynamic price:</span>
<span>
Decays with time, rises with demand. Dotted lines show potential
price paths based on different demand scenarios.
</span>
</p>
<SalesSwapsCountBadge isVisible={activeTab === "swaps"} />
</div>
<SimulatorChartArea />
</div>
Expand Down
20 changes: 17 additions & 3 deletions components/lbp-simulator/simulator/SimulatorStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { StatCard } from "./StatCard";
import { useShallow } from "zustand/react/shallow";
import { memo, useEffect } from "react";
import { calcTVLUSD } from "@/lib/lbp-math";
import { formatPrice } from "@/lib/utils";

function isEthOrWeth(
token: string,
Expand Down Expand Up @@ -85,11 +86,11 @@ const STAT_VALUE_SELECTORS: ValueSelector[] = [
},
(state) => {
const d = getDerived(state);
return `$${(d.startPrice * d.collateralUsd).toFixed(2)}`;
return formatPrice(d.startPrice * d.collateralUsd);
},
(state) => {
const d = getDerived(state);
return `$${d.tokenPriceUsd.toFixed(2)}`;
return formatPrice(d.tokenPriceUsd);
},
(state) => {
const d = getDerived(state);
Expand All @@ -99,6 +100,14 @@ const STAT_VALUE_SELECTORS: ValueSelector[] = [
const d = getDerived(state);
return `$${(d.tvlUsd / 1_000_000).toFixed(2)}M`;
},
(state) => {
const d = getDerived(state);
const last =
state.simulationData[state.simulationData.length - 1] ??
state.simulationData[0];
const priceInUsd = (last?.price ?? 0) * d.collateralUsd;
return formatPrice(priceInUsd);
},
];

/** Subscribes to store with a selector; only this (the value) re-renders when value changes. */
Expand Down Expand Up @@ -142,6 +151,11 @@ const STAT_META = [
description:
"Total value locked in the pool: collateral balance (e.g. USDC) plus token balance valued at current spot price in collateral.",
},
{
label: "Final (0 swaps)",
description:
"Final price if there were no trades at all (weight shift only; balances stay constant).",
},
] as const;

function SimulatorStatsComponent() {
Expand All @@ -160,7 +174,7 @@ function SimulatorStatsComponent() {
}, [config.collateralToken, ethPriceUsd, fetchEthPrice]);

return (
<div className="grid grid-cols-1 lg:grid-cols-6 md:grid-cols-4 sm:grid-cols-2 gap-4 mb-6">
<div className="grid grid-cols-1 lg:grid-cols-7 md:grid-cols-4 sm:grid-cols-2 gap-4 mb-6">
{STAT_META.map((meta, i) => (
<StatCard
key={meta.label}
Expand Down
3 changes: 2 additions & 1 deletion components/lbp-simulator/simulator/SwapFormSwapTabLive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useShallow } from "zustand/react/shallow";
import { useMemo, memo } from "react";
import { calculateOutGivenIn } from "@/lib/lbp-math";
import type { LBPConfig } from "@/lib/lbp-math";
import { formatPrice } from "@/lib/utils";

type SwapDirection = "buy" | "sell";

Expand Down Expand Up @@ -123,7 +124,7 @@ function SwapFormSwapTabLiveComponent({
<div className="flex justify-between">
<span>Price:</span>
<span className="font-medium text-foreground">
${priceUsd.toFixed(4)} {config.tokenSymbol}/{config.collateralToken}
{formatPrice(priceUsd)} {config.tokenSymbol}/{config.collateralToken}
</span>
</div>
</div>
Expand Down
7 changes: 5 additions & 2 deletions components/lbp-simulator/simulator/SwapFormTWAPTabLive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useShallow } from "zustand/react/shallow";
import { useMemo, memo } from "react";
import { Button } from "@/components/ui/button";
import type { LBPConfig } from "@/lib/lbp-math";
import { formatPrice } from "@/lib/utils";

/**
* Subscribes only to step-changing state (currentPrice) and userUsdcBalance.
Expand Down Expand Up @@ -75,7 +76,8 @@ function SwapFormTWAPTabLiveComponent({
if (part === "currentPrice") {
return (
<span className="ml-auto text-xs text-muted-foreground">
Current price: {currentPrice.toFixed(4)} {config.collateralToken} /{" "}
Current price: {formatPrice(currentPrice, { currencySymbol: "" })}{" "}
{config.collateralToken} /{" "}
{config.tokenSymbol}
</span>
);
Expand All @@ -92,7 +94,8 @@ function SwapFormTWAPTabLiveComponent({
maximumFractionDigits: 4,
})}{" "}
{config.tokenSymbol} @ ~
{currentPrice.toFixed(4)} {config.collateralToken}
{formatPrice(currentPrice, { currencySymbol: "" })}{" "}
{config.collateralToken}
</div>
</div>
);
Expand Down
Loading