Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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