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
3 changes: 2 additions & 1 deletion core/src/exchanges/gemini-titan/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
BuiltOrder,
} from '../../types';
import { AuthenticationError } from '../../errors';
import { toFixedDecimal } from '../../utils/decimal-math';
import { getGeminiConfig, GeminiApiConfig } from './config';
import { GeminiFetcher } from './fetcher';
import { GeminiNormalizer } from './normalizer';
Expand Down Expand Up @@ -230,7 +231,7 @@ export class GeminiTitanExchange extends PredictionMarketExchange {
orderType: 'limit',
side: params.side,
quantity: String(params.amount),
price: params.price !== undefined ? params.price.toFixed(2) : '0.50',
price: params.price !== undefined ? toFixedDecimal(params.price, 2) : '0.50',
outcome: side,
timeInForce: params.type === 'market' ? 'immediate-or-cancel' : 'good-til-cancel',
};
Expand Down
19 changes: 10 additions & 9 deletions core/src/exchanges/gemini-titan/normalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { IExchangeNormalizer } from '../interfaces';
import { addBinaryOutcomes } from '../../utils/market-utils';
import { buildSourceMetadata } from '../../utils/metadata';
import { averageDecimals, complementDecimal, multiplyDecimals, roundDecimalPlaces, subtractDecimals } from '../../utils/decimal-math';
import { toMarketId, toOutcomeId } from './utils';
import { TICK_SIZE } from './config';
import {
Expand Down Expand Up @@ -106,11 +107,8 @@ function extractDescription(desc: unknown): string {
return '';
}

/**
* Round to 2 decimal places to avoid floating point noise.
*/
function roundPrice(n: number): number {
return Math.round(n * 100) / 100;
return roundDecimalPlaces(n, 2);
}

// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -211,18 +209,20 @@ export class GeminiNormalizer implements IExchangeNormalizer<GeminiRawEvent, Gem
const marketId = toMarketId(instrumentSymbol);

// Extract prices
const bestBid = contract.prices?.bestBid ? parseFloat(contract.prices.bestBid) : 0.5;
const bestAsk = contract.prices?.bestAsk ? parseFloat(contract.prices.bestAsk) : 0.5;
const bestBidRaw = contract.prices?.bestBid ?? '0.5';
const bestAskRaw = contract.prices?.bestAsk ?? '0.5';
const bestBid = parseFloat(bestBidRaw);
const bestAsk = parseFloat(bestAskRaw);
const buyYes = contract.prices?.buy?.yes ? parseFloat(contract.prices.buy.yes) : undefined;
const sellYes = contract.prices?.sell?.yes ? parseFloat(contract.prices.sell.yes) : undefined;
const buyNo = contract.prices?.buy?.no ? parseFloat(contract.prices.buy.no) : undefined;
const sellNo = contract.prices?.sell?.no ? parseFloat(contract.prices.sell.no) : undefined;
const lastPrice = contract.prices?.lastTradePrice
? parseFloat(contract.prices.lastTradePrice)
: (bestBid + bestAsk) / 2;
: averageDecimals(bestBidRaw, bestAskRaw);

const yesPriceSource = buyYes ?? sellYes ?? lastPrice;
const noPriceSource = buyNo ?? sellNo ?? (1 - yesPriceSource);
const noPriceSource = buyNo ?? sellNo ?? complementDecimal(yesPriceSource);

const yesPrice = roundPrice(Math.max(0, Math.min(1, yesPriceSource)));
const noPrice = roundPrice(Math.max(0, Math.min(1, noPriceSource)));
Expand Down Expand Up @@ -325,6 +325,7 @@ export class GeminiNormalizer implements IExchangeNormalizer<GeminiRawEvent, Gem
: 0;
const entryPrice = parseFloat(raw.avgPrice);
const size = parseFloat(raw.totalQuantity);
const priceDelta = subtractDecimals(raw.prices?.bestBid ?? '0', raw.avgPrice);

return {
marketId: toMarketId(raw.symbol),
Expand All @@ -333,7 +334,7 @@ export class GeminiNormalizer implements IExchangeNormalizer<GeminiRawEvent, Gem
size,
entryPrice,
currentPrice,
unrealizedPnL: (currentPrice - entryPrice) * size,
unrealizedPnL: multiplyDecimals(priceDelta, raw.totalQuantity),
};
}
}
9 changes: 5 additions & 4 deletions core/src/exchanges/kalshi/normalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { UnifiedMarket, UnifiedEvent, UnifiedSeries, PriceCandle, OrderBook, Tra
import { IExchangeNormalizer } from '../interfaces';
import { addBinaryOutcomes } from '../../utils/market-utils';
import { buildSourceMetadata } from '../../utils/metadata';
import { averageDecimals, complementDecimal, subtractDecimals } from '../../utils/decimal-math';
import { fromKalshiCents, invertKalshiUnified } from './price';
import { KalshiRawEvent, KalshiRawMarket, KalshiRawCandlestick, KalshiRawTrade, KalshiRawFill, KalshiRawOrder, KalshiRawPosition, KalshiRawOrderBookFp, KalshiRawSeries } from './fetcher';

Expand Down Expand Up @@ -51,7 +52,7 @@ export class KalshiNormalizer implements IExchangeNormalizer<KalshiRawEvent, Kal
if (market.last_price_dollars != null) {
price = parseFloat(market.last_price_dollars);
} else if (market.yes_ask_dollars != null && market.yes_bid_dollars != null) {
price = (parseFloat(market.yes_ask_dollars) + parseFloat(market.yes_bid_dollars)) / 2;
price = averageDecimals(market.yes_ask_dollars, market.yes_bid_dollars);
} else if (market.yes_ask_dollars != null) {
price = parseFloat(market.yes_ask_dollars);
} else if (market.last_price) {
Expand All @@ -66,7 +67,7 @@ export class KalshiNormalizer implements IExchangeNormalizer<KalshiRawEvent, Kal

let priceChange = 0;
if (market.previous_price_dollars != null && market.last_price_dollars != null) {
priceChange = parseFloat(market.last_price_dollars) - parseFloat(market.previous_price_dollars);
priceChange = subtractDecimals(market.last_price_dollars, market.previous_price_dollars);
}

const outcomes: MarketOutcome[] = [
Expand Down Expand Up @@ -222,7 +223,7 @@ export class KalshiNormalizer implements IExchangeNormalizer<KalshiRawEvent, Kal
size: parseFloat(level[1]),
}));
asks = (data.yes_dollars || []).map((level) => ({
price: Math.round((1 - parseFloat(level[0])) * 10000) / 10000,
price: complementDecimal(level[0], 4),
size: parseFloat(level[1]),
}));
} else {
Expand All @@ -231,7 +232,7 @@ export class KalshiNormalizer implements IExchangeNormalizer<KalshiRawEvent, Kal
size: parseFloat(level[1]),
}));
asks = (data.no_dollars || []).map((level) => ({
price: Math.round((1 - parseFloat(level[0])) * 10000) / 10000,
price: complementDecimal(level[0], 4),
size: parseFloat(level[1]),
}));
}
Expand Down
10 changes: 6 additions & 4 deletions core/src/exchanges/opinion/normalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { IExchangeNormalizer } from '../interfaces';
import { addBinaryOutcomes } from '../../utils/market-utils';
import { buildSourceMetadata } from '../../utils/metadata';
import { divideDecimals, proportionalDecimal, subtractDecimals } from '../../utils/decimal-math';
import { parseNumStr, mapOrderStatus, toMillis, intervalToMs } from './utils';
import {
OpinionRawMarket,
Expand Down Expand Up @@ -83,9 +84,8 @@ export class OpinionNormalizer implements IExchangeNormalizer<OpinionRawMarket,
const totalChildVolume = children.reduce((sum, c) => sum + parseNumStr(c.volume), 0);

for (const child of children) {
const childVolume = parseNumStr(child.volume);
const childVolume24h = totalChildVolume > 0
? (childVolume / totalChildVolume) * parentVolume24h
? proportionalDecimal(child.volume || '0', totalChildVolume, parentVolume24h, 6)
: 0;
const market = this.normalizeChildMarket(child, raw, childVolume24h);
if (market) results.push(market);
Expand Down Expand Up @@ -243,7 +243,9 @@ export class OpinionNormalizer implements IExchangeNormalizer<OpinionRawMarket,
normalizePosition(raw: OpinionRawPosition): Position {
const sharesOwned = parseNumStr(raw.sharesOwned);
const currentValue = parseNumStr(raw.currentValueInQuoteToken);
const currentPrice = sharesOwned > 0 ? currentValue / sharesOwned : 0;
const currentPrice = sharesOwned > 0
? divideDecimals(raw.currentValueInQuoteToken || '0', raw.sharesOwned || '0')
: 0;

return {
marketId: String(raw.marketId),
Expand Down Expand Up @@ -272,7 +274,7 @@ export class OpinionNormalizer implements IExchangeNormalizer<OpinionRawMarket,
amount: orderShares,
status: mapOrderStatus(raw.status),
filled: filledShares,
remaining: orderShares - filledShares,
remaining: subtractDecimals(raw.orderShares || '0', raw.filledShares || '0'),
timestamp: toMillis(raw.createdAt) ?? 0,
};
}
Expand Down
14 changes: 5 additions & 9 deletions core/src/exchanges/polymarket_us/price.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { OrderIntent, Amount } from 'polymarket-us';
import { complementDecimal, roundToTickDecimal } from '../../utils/decimal-math';

/**
* Polymarket US price/quantity conversion utilities.
Expand Down Expand Up @@ -37,10 +38,8 @@ const LONG_INTENTS: ReadonlySet<OrderIntent> = new Set<OrderIntent>([
]);

/**
* Round a price to a tick size using Math.round. Defaults to the
* Round a price to a tick size using decimal string arithmetic. Defaults to the
* Polymarket US tick size (0.001) but accepts a per-market override.
* Re-rounds to `POLYMARKET_US_PRICE_DECIMALS` afterwards to avoid
* floating-point drift.
*
* Note: this is a pure rounding helper - it does NOT validate bounds.
* Out-of-range inputs (e.g. 0.9999 -> 1.000) will pass through unchanged.
Expand All @@ -49,10 +48,7 @@ export function roundToTickSize(
price: number,
tickSize: number = POLYMARKET_US_TICK_SIZE,
): number {
const ticks = Math.round(price / tickSize);
const rounded = ticks * tickSize;
const scale = Math.pow(10, POLYMARKET_US_PRICE_DECIMALS);
return Math.round(rounded * scale) / scale;
return roundToTickDecimal(price, tickSize, POLYMARKET_US_PRICE_DECIMALS);
}

/**
Expand Down Expand Up @@ -94,7 +90,7 @@ export function toLongSidePrice(intent: OrderIntent, userPrice: number): number
if (LONG_INTENTS.has(intent)) {
return userPrice;
}
const longPrice = 1 - userPrice;
const longPrice = complementDecimal(userPrice, POLYMARKET_US_PRICE_DECIMALS);
validatePriceBounds(longPrice);
return longPrice;
}
Expand All @@ -114,7 +110,7 @@ export function fromLongSidePrice(intent: OrderIntent, longPrice: number): numbe
if (LONG_INTENTS.has(intent)) {
return longPrice;
}
const userPrice = 1 - longPrice;
const userPrice = complementDecimal(longPrice, POLYMARKET_US_PRICE_DECIMALS);
validatePriceBounds(userPrice);
return userPrice;
}
Expand Down
4 changes: 3 additions & 1 deletion core/src/exchanges/smarkets/normalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { UnifiedMarket, UnifiedEvent, OrderBook, Trade, UserTrade, Position, Bal
import { IExchangeNormalizer } from '../interfaces';
import { addBinaryOutcomes } from '../../utils/market-utils';
import { buildSourceMetadata } from '../../utils/metadata';
import { subtractDecimals } from '../../utils/decimal-math';
import { fromBasisPoints, fromQuantityUnits } from './price';
import {
SmarketsRawEventWithMarkets,
Expand Down Expand Up @@ -304,12 +305,13 @@ export class SmarketsNormalizer implements IExchangeNormalizer<SmarketsRawEventW
normalizeBalance(raw: SmarketsRawBalance): Balance[] {
const balance = parseFloat(raw.balance || '0');
const available = parseFloat(raw.available_balance || '0');
const locked = subtractDecimals(raw.balance || '0', raw.available_balance || '0');

return [{
currency: raw.currency || 'GBP',
total: balance,
available,
locked: balance - available,
locked,
}];
}

Expand Down
6 changes: 4 additions & 2 deletions core/src/exchanges/smarkets/price.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* and 1/10000 GBP units for quantities.
*/

import { toScaledInteger } from '../../utils/decimal-math';

const BASIS_POINTS_SCALE = 10000;

/**
Expand All @@ -18,7 +20,7 @@ export function fromBasisPoints(basisPoints: number): number {
* Convert probability (0.0-1.0) to Smarkets basis points (0-10000).
*/
export function toBasisPoints(probability: number): number {
return Math.round(probability * BASIS_POINTS_SCALE);
return toScaledInteger(probability, BASIS_POINTS_SCALE);
}

/**
Expand All @@ -32,7 +34,7 @@ export function fromQuantityUnits(units: number): number {
* Convert GBP to Smarkets quantity units (1/10000 GBP).
*/
export function toQuantityUnits(gbp: number): number {
return Math.round(gbp * BASIS_POINTS_SCALE);
return toScaledInteger(gbp, BASIS_POINTS_SCALE);
}

/**
Expand Down
Loading
Loading