(null);
return (
@@ -106,25 +131,33 @@ export function OrderTicket({
-
- {offsetBps === 0 ? 'At mid' : side === 'buy' ? 'Below mid' : 'Above mid'}
-
-
- {OFFSETS.map((bps) => (
-
- ))}
+
+
+ {target === spotWad ? 'At mid' : target < spotWad ? 'Below mid' : 'Above mid'}
+
+
+ {signed}
+
+
+ ({
+ value: bps,
+ label: bps === 0 ? '0%' : formatBps(bps),
+ }))}
+ aria-label="Offset from mid"
+ />
+
+ {overrideActive ? (
+
+ Custom price set — move the slider to clear it.
+
+ ) : null}
Include when price is {side === 'buy' ? '≤' : '≥'}
-
- ${formatPrice(target)}
-
+ $
+ setPriceText(formatPrice(target))}
+ onChange={(event) => {
+ const text = event.target.value;
+ setPriceText(text);
+ const wad = parsePriceWad(text);
+ if (wad !== null) onPriceOverride(wad);
+ }}
+ onBlur={() => setPriceText(null)}
+ className={`w-fit min-w-0 max-w-full cursor-text border-b border-dashed bg-transparent tabular-nums outline-none transition-colors focus:border-solid ${
+ side === 'buy'
+ ? 'border-bds-green-70/40 hover:border-bds-green-70 focus:border-bds-green-70'
+ : 'border-bds-red-70/40 hover:border-bds-red-70 focus:border-bds-red-70'
+ }`}
+ size={Math.max((priceText ?? formatPrice(target)).length, 4)}
+ />
+
- mid {signed}
+ mid {signed} · {overrideActive ? 'custom price' : 'type to set a price'}
diff --git a/app/vibenet/demos/validity/lib/predicates.test.ts b/app/vibenet/demos/validity/lib/predicates.test.ts
index 6229aeac..0e0e6d4c 100644
--- a/app/vibenet/demos/validity/lib/predicates.test.ts
+++ b/app/vibenet/demos/validity/lib/predicates.test.ts
@@ -6,6 +6,7 @@ import {
blockExpiryPredicate,
blockNumberPredicate,
formatPrice,
+ parsePriceWad,
prettyValidity,
priceValidity,
rectangleForTarget,
@@ -29,6 +30,27 @@ describe('predicates', () => {
expect(formatPrice(99n * 10n ** 16n)).toBe('0.9900');
});
+ it('parses typed prices into wad', () => {
+ expect(parsePriceWad('1')).toBe(WAD);
+ expect(parsePriceWad('1.0000')).toBe(WAD);
+ expect(parsePriceWad('$0.99')).toBe(99n * 10n ** 16n);
+ expect(parsePriceWad('.5')).toBe(WAD / 2n);
+ expect(parsePriceWad('1,024.5')).toBe(1_024n * WAD + WAD / 2n);
+ expect(parsePriceWad(' 2.5 ')).toBe((5n * WAD) / 2n);
+ expect(parsePriceWad(formatPrice((3n * WAD) / 2n))).toBe((3n * WAD) / 2n);
+ });
+
+ it('rejects non-prices', () => {
+ expect(parsePriceWad('')).toBeNull();
+ expect(parsePriceWad('0')).toBeNull();
+ expect(parsePriceWad('0.000')).toBeNull();
+ expect(parsePriceWad('-1')).toBeNull();
+ expect(parsePriceWad('1e3')).toBeNull();
+ expect(parsePriceWad('1.2.3')).toBeNull();
+ expect(parsePriceWad('abc')).toBeNull();
+ expect(parsePriceWad('1.0000000000000000001')).toBeNull();
+ });
+
it('offsets spot in basis points for buy and sell', () => {
expect(applyOffsetBps(WAD, 'buy', 100)).toBe((99n * WAD) / 100n);
expect(applyOffsetBps(WAD, 'sell', 100)).toBe((101n * WAD) / 100n);
@@ -36,6 +58,44 @@ describe('predicates', () => {
expect(applyOffsetBps(WAD, 'sell', 0)).toBe(WAD);
});
+ it('stretches a satisfied buy box to the current point', () => {
+ const k = 1_000n * WAD * (1_000n * WAD); // mid 1.0
+ const current = WAD;
+ const target = 2n * WAD; // buy at ≤ 2 with mid at 1: already satisfied
+ const box = rectangleForTarget(k, target, 'buy', current);
+ const r0Now = sqrt((k * WAD) / current);
+ const r1Now = k / r0Now;
+ expect(box.r0Min <= r0Now && r0Now <= box.r0Max).toBe(true);
+ expect(box.r1Min <= r1Now && r1Now <= box.r1Max).toBe(true);
+ // Corners still respect price ≤ P.
+ expect((box.r1Max * WAD) / box.r0Min <= target).toBe(true);
+ });
+
+ it('stretches a satisfied sell box to the current point', () => {
+ const k = 1_000n * WAD * (1_000n * WAD); // mid 1.0
+ const current = WAD;
+ const target = WAD / 2n; // sell at ≥ 0.5 with mid at 1: already satisfied
+ const box = rectangleForTarget(k, target, 'sell', current);
+ const r0Now = sqrt((k * WAD) / current);
+ const r1Now = k / r0Now;
+ expect(box.r0Min <= r0Now && r0Now <= box.r0Max).toBe(true);
+ expect(box.r1Min <= r1Now && r1Now <= box.r1Max).toBe(true);
+ // Corners still respect price ≥ P.
+ expect((box.r1Min * WAD) / box.r0Max >= target).toBe(true);
+ });
+
+ it('keeps the resting box when the condition is not yet met', () => {
+ const k = 1_000n * WAD * (1_000n * WAD); // mid 1.0
+ const buyTarget = (99n * WAD) / 100n; // buy below mid rests as before
+ expect(rectangleForTarget(k, buyTarget, 'buy', WAD)).toEqual(
+ rectangleForTarget(k, buyTarget, 'buy'),
+ );
+ const sellTarget = (101n * WAD) / 100n; // sell above mid rests as before
+ expect(rectangleForTarget(k, sellTarget, 'sell', WAD)).toEqual(
+ rectangleForTarget(k, sellTarget, 'sell'),
+ );
+ });
+
it('buy box implies every corner has price ≤ P', () => {
const k = 1_000n * WAD * (1_000n * WAD);
const target = (99n * WAD) / 100n;
diff --git a/app/vibenet/demos/validity/lib/predicates.ts b/app/vibenet/demos/validity/lib/predicates.ts
index 70492362..98c8db89 100644
--- a/app/vibenet/demos/validity/lib/predicates.ts
+++ b/app/vibenet/demos/validity/lib/predicates.ts
@@ -44,6 +44,16 @@ export function formatPrice(wad: bigint, digits = 4): string {
return `${negative ? '-' : ''}${int.toString()}.${frac}`;
}
+/** Parse a typed price like "1.0421", "$0.98", or "1,024.5" into wad. Null if not a positive price. */
+export function parsePriceWad(input: string): bigint | null {
+ const cleaned = input.trim().replace(/^\$/, '').replace(/,/g, '');
+ if (!/^(\d+(\.\d*)?|\.\d+)$/.test(cleaned)) return null;
+ const [intPart = '0', fracPart = ''] = cleaned.split('.');
+ if (fracPart.length > 18) return null;
+ const wad = BigInt(intPart || '0') * WAD + BigInt(fracPart.padEnd(18, '0') || '0');
+ return wad > 0n ? wad : null;
+}
+
/** Apply a basis-point offset to spot. Buy is below (`-bps`), sell is above (`+bps`). 0 is at mid. */
export function applyOffsetBps(spotWad: bigint, side: Side, offsetBps: number): bigint {
if (spotWad <= 0n) throw new Error('Need a live mid price.');
@@ -83,17 +93,33 @@ export function prettyValidity(predicates: ValidityPredicate[]): string {
* sell (price ≥ P): A/s ≤ r0 ≤ A ∧ B ≤ r1 ≤ B·s with B/A ≥ P
*
* Four storage predicates, so a drained or wildly expanded pool cannot fill.
+ *
+ * When `currentPriceWad` already satisfies the condition (a buy priced above
+ * the mid, a sell priced below it), the box stretches along the hyperbola to
+ * the current point so the order fills without the mid retracing to target.
*/
-export function rectangleForTarget(k: bigint, targetPriceWad: bigint, side: Side): Rectangle {
+export function rectangleForTarget(
+ k: bigint,
+ targetPriceWad: bigint,
+ side: Side,
+ currentPriceWad?: bigint,
+): Rectangle {
if (k === 0n || targetPriceWad <= 0n) {
throw new Error('Need a live pool and a positive target price.');
}
const a = sqrt((k * WAD) / targetPriceWad);
if (a === 0n) throw new Error('Degenerate reserve bound.');
+ const aCur =
+ currentPriceWad !== undefined && currentPriceWad > 0n
+ ? sqrt((k * WAD) / currentPriceWad)
+ : a;
if (side === 'buy') {
const b = (a * targetPriceWad) / WAD || 1n;
- const r0Max = (a * BOX_SPAN_NUM) / BOX_SPAN_DEN;
- const r1Min = (b * BOX_SPAN_DEN) / BOX_SPAN_NUM;
+ // Larger r0 means lower price; a satisfied buy sits at aOut > a.
+ const aOut = aCur > a ? aCur : a;
+ const bOut = k / aOut || 1n;
+ const r0Max = (aOut * BOX_SPAN_NUM) / BOX_SPAN_DEN;
+ const r1Min = (bOut * BOX_SPAN_DEN) / BOX_SPAN_NUM;
return {
r0Min: a,
r0Max: r0Max > a ? r0Max : a + 1n,
@@ -103,8 +129,11 @@ export function rectangleForTarget(k: bigint, targetPriceWad: bigint, side: Side
};
}
const b = (a * targetPriceWad + WAD - 1n) / WAD;
- const r0Min = (a * BOX_SPAN_DEN) / BOX_SPAN_NUM;
- const r1Max = (b * BOX_SPAN_NUM) / BOX_SPAN_DEN;
+ // Smaller r0 means higher price; a satisfied sell sits at aOut < a.
+ const aOut = aCur < a && aCur > 0n ? aCur : a;
+ const bOut = (k + aOut - 1n) / aOut;
+ const r0Min = (aOut * BOX_SPAN_DEN) / BOX_SPAN_NUM;
+ const r1Max = (bOut * BOX_SPAN_NUM) / BOX_SPAN_DEN;
return {
r0Min: r0Min < a ? r0Min : 1n,
r0Max: a,
@@ -141,8 +170,9 @@ export function priceValidity(
k: bigint,
targetPriceWad: bigint,
side: Side,
+ currentPriceWad?: bigint,
): { rectangle: Rectangle; predicates: ValidityPredicate[] } {
- const rectangle = rectangleForTarget(k, targetPriceWad, side);
+ const rectangle = rectangleForTarget(k, targetPriceWad, side, currentPriceWad);
const r1MinValue = rectangle.r1Min << RESERVE_BITS;
const r1MaxValue = rectangle.r1Max << RESERVE_BITS;
const predicates: ValidityPredicate[] = [