UNPKG

ox

Version:

Ethereum Standard Library

282 lines (243 loc) 9.84 kB
import { Tick } from 'ox/tempo' import { describe, expect, test } from 'vitest' describe('toPrice', () => { test('converts tick 0 to price 1', () => { expect(Tick.toPrice(0)).toBe('1') }) test('converts positive ticks correctly', () => { expect(Tick.toPrice(100)).toBe('1.001') expect(Tick.toPrice(1000)).toBe('1.01') expect(Tick.toPrice(2000)).toBe('1.02') }) test('converts negative ticks correctly', () => { expect(Tick.toPrice(-100)).toBe('0.999') expect(Tick.toPrice(-1000)).toBe('0.99') expect(Tick.toPrice(-2000)).toBe('0.98') }) test('handles boundary values', () => { expect(Tick.toPrice(Tick.minTick)).toBe('0.98') expect(Tick.toPrice(Tick.maxTick)).toBe('1.02') }) test('preserves exact 5 decimal precision', () => { expect(Tick.toPrice(1)).toBe('1.00001') expect(Tick.toPrice(-1)).toBe('0.99999') expect(Tick.toPrice(1234)).toBe('1.01234') }) test('throws error when tick is below minimum', () => { expect(() => Tick.toPrice(-2001)).toThrow(Tick.TickOutOfBoundsError) expect(() => Tick.toPrice(-2001)).toThrow('Tick -2001 is out of bounds.') expect(() => Tick.toPrice(-3000)).toThrow(Tick.TickOutOfBoundsError) }) test('throws error when tick is above maximum', () => { expect(() => Tick.toPrice(2001)).toThrow(Tick.TickOutOfBoundsError) expect(() => Tick.toPrice(2001)).toThrow('Tick 2001 is out of bounds.') expect(() => Tick.toPrice(3000)).toThrow(Tick.TickOutOfBoundsError) }) test('handles edge cases near bounds', () => { expect(() => Tick.toPrice(-2000)).not.toThrow() expect(() => Tick.toPrice(2000)).not.toThrow() }) }) describe('fromPrice', () => { test('converts price 1.0 to tick 0', () => { expect(Tick.fromPrice('1.0')).toBe(0) expect(Tick.fromPrice('1.00000')).toBe(0) }) test('converts prices above 1.0 correctly', () => { expect(Tick.fromPrice('1.001')).toBe(100) expect(Tick.fromPrice('1.01')).toBe(1000) expect(Tick.fromPrice('1.02')).toBe(2000) }) test('converts prices below 1.0 correctly', () => { expect(Tick.fromPrice('0.999')).toBe(-100) expect(Tick.fromPrice('0.99')).toBe(-1000) expect(Tick.fromPrice('0.98')).toBe(-2000) }) test('handles boundary values', () => { expect(Tick.fromPrice('0.98')).toBe(Tick.minTick) expect(Tick.fromPrice('1.02')).toBe(Tick.maxTick) }) test('handles different decimal precisions', () => { expect(Tick.fromPrice('1.00001')).toBe(1) expect(Tick.fromPrice('1.0001')).toBe(10) expect(Tick.fromPrice('1.001')).toBe(100) expect(Tick.fromPrice('0.99999')).toBe(-1) }) test('truncates beyond 5 decimal places', () => { // Should truncate, not round, extra decimals expect(Tick.fromPrice('1.000019')).toBe(1) // Takes first 5: 1.00001 expect(Tick.fromPrice('1.000015')).toBe(1) // Takes first 5: 1.00001 }) test('validates string format', () => { expect(() => Tick.fromPrice('abc')).toThrow(Tick.InvalidPriceFormatError) expect(() => Tick.fromPrice('abc')).toThrow('Invalid price format') expect(() => Tick.fromPrice('1.2.3')).toThrow(Tick.InvalidPriceFormatError) expect(() => Tick.fromPrice('')).toThrow(Tick.InvalidPriceFormatError) }) test('throws error when price results in tick below minimum', () => { expect(() => Tick.fromPrice('0.979')).toThrow(Tick.PriceOutOfBoundsError) expect(() => Tick.fromPrice('0.979')).toThrow( 'Price "0.979" results in tick -2100 which is out of bounds.', ) expect(() => Tick.fromPrice('0.5')).toThrow(Tick.PriceOutOfBoundsError) }) test('throws error when price results in tick above maximum', () => { expect(() => Tick.fromPrice('1.021')).toThrow(Tick.PriceOutOfBoundsError) expect(() => Tick.fromPrice('1.021')).toThrow( 'Price "1.021" results in tick 2100 which is out of bounds.', ) expect(() => Tick.fromPrice('1.5')).toThrow(Tick.PriceOutOfBoundsError) }) test('handles edge cases near bounds', () => { expect(() => Tick.fromPrice('0.98')).not.toThrow() expect(() => Tick.fromPrice('1.02')).not.toThrow() }) test('handles whitespace', () => { expect(Tick.fromPrice(' 1.0 ')).toBe(0) expect(Tick.fromPrice(' 1.001 ')).toBe(100) }) }) describe('round-trip conversions', () => { test('tick -> price -> tick preserves tick values exactly', () => { const ticks = [-2000, -1000, -100, -1, 0, 1, 100, 1000, 2000] for (const tick of ticks) { const price = Tick.toPrice(tick) const roundTripTick = Tick.fromPrice(price) expect(roundTripTick).toBe(tick) } }) test('price -> tick -> price preserves price strings exactly', () => { const prices = ['0.98', '0.99', '1', '1.01', '1.02'] for (const price of prices) { const tick = Tick.fromPrice(price) const roundTripPrice = Tick.toPrice(tick) expect(roundTripPrice).toBe(price) } }) test('handles arbitrary precision in input strings', () => { // Shorter decimal strings should round-trip correctly expect(Tick.toPrice(Tick.fromPrice('1.0'))).toBe('1') expect(Tick.toPrice(Tick.fromPrice('0.999'))).toBe('0.999') // Longer decimal strings get truncated to 5 decimals expect(Tick.toPrice(Tick.fromPrice('1.000019999'))).toBe('1.00001') }) }) describe('error handling', () => { test('InvalidPriceFormatError is catchable and has correct properties', () => { try { Tick.fromPrice('invalid') expect.fail('Should have thrown') } catch (error) { expect(error).toBeInstanceOf(Tick.InvalidPriceFormatError) expect(error).toHaveProperty('name', 'Tick.InvalidPriceFormatError') expect((error as Error).message).toContain('Invalid price format') expect((error as Error).message).toContain('invalid') expect((error as Error).message).toContain( 'Price must be a decimal number string', ) } }) test('TickOutOfBoundsError is catchable and has correct properties', () => { try { Tick.toPrice(-2001) expect.fail('Should have thrown') } catch (error) { expect(error).toBeInstanceOf(Tick.TickOutOfBoundsError) expect(error).toHaveProperty('name', 'Tick.TickOutOfBoundsError') // BaseError appends metaMessages to the message expect((error as Error).message).toContain('Tick -2001 is out of bounds.') expect((error as Error).message).toContain( 'Tick must be between -2000 and 2000.', ) } }) test('PriceOutOfBoundsError is catchable and has correct properties', () => { try { Tick.fromPrice('0.979') expect.fail('Should have thrown') } catch (error) { expect(error).toBeInstanceOf(Tick.PriceOutOfBoundsError) expect(error).toHaveProperty('name', 'Tick.PriceOutOfBoundsError') expect((error as Error).message).toContain( 'Price "0.979" results in tick -2100 which is out of bounds.', ) expect((error as Error).message).toContain( 'Tick must be between -2000 and 2000.', ) } }) test('can distinguish between error types', () => { try { Tick.fromPrice('invalid') } catch (error) { if (error instanceof Tick.InvalidPriceFormatError) { expect(true).toBe(true) // Successfully caught as InvalidPriceFormatError } else { expect.fail('Should be InvalidPriceFormatError') } } try { Tick.toPrice(-2001) } catch (error) { if (error instanceof Tick.TickOutOfBoundsError) { expect(true).toBe(true) // Successfully caught as TickOutOfBoundsError } else { expect.fail('Should be TickOutOfBoundsError') } } try { Tick.fromPrice('0.979') } catch (error) { if (error instanceof Tick.PriceOutOfBoundsError) { expect(true).toBe(true) // Successfully caught as PriceOutOfBoundsError } else { expect.fail('Should be PriceOutOfBoundsError') } } }) }) describe('precision and edge cases', () => { test('handles very small price increments', () => { // 0.1 bps = 0.001% = 0.00001 in decimal expect(Tick.fromPrice('1.00001')).toBe(1) expect(Tick.fromPrice('1.00002')).toBe(2) }) test('price scale maintains exact 5 decimal precision', () => { // The price scale is 100,000 which gives us exactly 5 decimal places const price0 = Tick.toPrice(0) const price1 = Tick.toPrice(1) expect(price0).toBe('1') expect(price1).toBe('1.00001') // Verify exact difference const diff = Number(price1) - Number(price0) expect(diff).toBeCloseTo(0.00001, 10) }) test('symmetric around 1.0', () => { // +100 ticks should have same magnitude as -100 ticks from 1.0 const priceUp = Tick.toPrice(100) const priceDown = Tick.toPrice(-100) expect(priceUp).toBe('1.001') expect(priceDown).toBe('0.999') const upDiff = Number(priceUp) - 1.0 const downDiff = 1.0 - Number(priceDown) expect(upDiff).toBeCloseTo(downDiff, 10) }) test('validates 2% range', () => { // At minimum tick, price should be exactly 2% below 1.0 const minPrice = Tick.toPrice(Tick.minTick) expect(minPrice).toBe('0.98') expect((1.0 - Number(minPrice)) / 1.0).toBeCloseTo(0.02, 10) // At maximum tick, price should be exactly 2% above 1.0 const maxPrice = Tick.toPrice(Tick.maxTick) expect(maxPrice).toBe('1.02') expect((Number(maxPrice) - 1.0) / 1.0).toBeCloseTo(0.02, 10) }) test('no floating point precision errors', () => { // String-based parsing should eliminate float precision issues expect(Tick.fromPrice('0.99999')).toBe(-1) expect(Tick.fromPrice('1.00001')).toBe(1) // These would potentially fail with float arithmetic expect(Tick.fromPrice('0.98001')).toBe(-1999) expect(Tick.fromPrice('1.01999')).toBe(1999) }) })