Skip to content
markpaper

src/orders/rules.test.ts

v0.1.0 · 5.1 KB

Download file
import { describe, expect, it } from 'vitest';
import { createQuant } from '../numbers/quant.js';
import { decodeWriteAnswer } from '../signer/decode.js';
import { signature } from '../testing/fixtures.js';
import {
  cancelOutcomeFor,
  chunkCancelIds,
  clampIocToBand,
  createCancelBatcher,
  createIntentIds,
  iocLimitTicks,
  planReduceOnly,
  resolveIocFill,
} from './rules.js';

const q = createQuant({ tickSize: 100n, baseLotsDecimals: 4 });
const cpi = { priceInTicks: 0n, seq: 0n, basePosted: 0n, filledBaseLots: 2n, filledQuoteLots: 300n };
describe('order rules', () => {
  it('uses caller-defined identity prefixes and never truncates the unique suffix', () => {
    const gen = createIntentIds({ prefix: 'example' });
    const ids = new Set(Array.from({ length: 100 }, () => gen.next('order', 'BTC')));
    expect(ids.size).toBe(100);
    for (const id of ids) expect(id).toMatch(/^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/);
    expect(() => createIntentIds({ prefix: '' })).toThrow();
  });
  it('rounds caps in the protective direction', () => {
    expect(iocLimitTicks(q, 800.5, true)).toBe(800n);
    expect(iocLimitTicks(q, 800.5, false)).toBe(801n);
  });
  it('never weakens the caller cap when clamping an optional band', () => {
    expect(clampIocToBand(q, 25n, true, { min: 10, max: 20 })).toBe(20n);
    expect(clampIocToBand(q, 5n, true, { min: 10, max: 20 })).toBe(0n);
    expect(clampIocToBand(q, 5n, false, { min: 10, max: 20 })).toBe(10n);
    expect(clampIocToBand(q, 25n, false, { min: 10, max: 20 })).toBe(0n);
  });
  it('never sends reduce-only without a reducible position', () => {
    expect(planReduceOnly({ positionLots: 0n, requestedLots: 1n, isBuy: false, fullClose: true }).send).toBe(false);
    expect(planReduceOnly({ positionLots: 3n, requestedLots: 1n, isBuy: true, fullClose: false }).send).toBe(false);
  });
  it('full close takes exact position lots and partial close clamps', () => {
    expect(planReduceOnly({ positionLots: 3n, requestedLots: 0n, isBuy: false, fullClose: true })).toEqual({
      send: true,
      lots: 3n,
    });
    expect(planReduceOnly({ positionLots: -3n, requestedLots: 10n, isBuy: true, fullClose: false })).toEqual({
      send: true,
      lots: 3n,
    });
  });
  it('uses the smaller of transaction evidence and position delta', () => {
    expect(
      resolveIocFill({ cpi, sentLots: 3n, isBuy: true, beforeLots: 0n, read: { lots: 1n, slot: 10 }, txSlot: 10 }),
    ).toEqual({ filledLots: 1n });
    expect(
      resolveIocFill({ cpi, sentLots: 1n, isBuy: true, beforeLots: 0n, read: { lots: 3n, slot: 10 }, txSlot: 10 }),
    ).toEqual({ filledLots: 1n });
  });
  it('accepts delta-only at exactly the transaction slot', () => {
    expect(
      resolveIocFill({
        cpi: null,
        sentLots: 3n,
        isBuy: true,
        beforeLots: 0n,
        read: { lots: 1n, slot: 10 },
        txSlot: 10,
      }),
    ).toEqual({ filledLots: 1n });
    expect(
      resolveIocFill({
        cpi: null,
        sentLots: 3n,
        isBuy: true,
        beforeLots: 0n,
        read: { lots: 1n, slot: 11 },
        txSlot: 10,
      }),
    ).toHaveProperty('rejected', true);
  });
  it('rejects an old read and a zero reported transaction fill', () => {
    expect(
      resolveIocFill({ cpi, sentLots: 3n, isBuy: true, beforeLots: 0n, read: { lots: 1n, slot: 9 }, txSlot: 10 }),
    ).toHaveProperty('rejected', true);
    expect(
      resolveIocFill({
        cpi: { ...cpi, filledBaseLots: 0n },
        sentLots: 3n,
        isBuy: true,
        beforeLots: 0n,
        read: { lots: 1n, slot: 10 },
        txSlot: 10,
      }),
    ).toHaveProperty('rejected', true);
  });
  it('does not count cold/no-op or not-found cancels', () => {
    const id = { symbol: 'BTC', priceTicks: '800', seq: '3' };
    expect(
      cancelOutcomeFor(
        decodeWriteAnswer({ status: 'confirmed', signature, slot: 10, cancel: { effective: false, notFound: [] } }),
        id,
      ),
    ).toBe(false);
    expect(
      cancelOutcomeFor(
        decodeWriteAnswer({
          status: 'confirmed',
          signature,
          slot: 10,
          cancel: { effective: true, notFound: [{ priceInTicks: '800', seq: '3', reason: 'OrderNotFound' }] },
        }),
        id,
      ),
    ).toBe(false);
    expect(
      cancelOutcomeFor(
        decodeWriteAnswer({ status: 'confirmed', signature, slot: 10, cancel: { effective: true, notFound: [] } }),
        id,
      ),
    ).toBe(true);
  });
  it('batches exact IDs and learns a smaller maximum only on definite size/CU failure', async () => {
    expect(
      chunkCancelIds(
        Array.from({ length: 40 }, (_, i) => i),
        30,
      ).map((c) => c.length),
    ).toEqual([30, 10]);
    const sizes: number[] = [];
    const b = createCancelBatcher<number>({
      max: 6,
      send: async (ids) => {
        sizes.push(ids.length);
        return ids.length > 3 ? { ok: false, reason: 'ComputationalBudgetExceeded' } : { ok: true };
      },
    });
    const r = await b.cancel([1, 2, 3, 4, 5, 6, 7]);
    expect(sizes).toEqual([6, 3, 3, 1]);
    expect(r.every((x) => x.result.ok)).toBe(true);
    expect(b.max()).toBe(3);
  });
});
All files