Skip to content
Merged
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
38 changes: 38 additions & 0 deletions core/src/exchanges/baozi/price.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { clampBaoziPrice, normalizeBaoziOutcomes } from "./price";
import { MarketOutcome } from "../../types";

describe("clampBaoziPrice", () => {
test("clamps values below 0 to 0", () => {
expect(clampBaoziPrice(-0.1)).toBe(0);
});

test("clamps values above 1 to 1", () => {
expect(clampBaoziPrice(1.2)).toBe(1);
});

test("leaves values in range unchanged", () => {
expect(clampBaoziPrice(0.3)).toBe(0.3);
expect(clampBaoziPrice(0)).toBe(0);
expect(clampBaoziPrice(1)).toBe(1);
});
});

describe("normalizeBaoziOutcomes", () => {
function makeOutcome(price: number): MarketOutcome {
return { outcomeId: "x", marketId: "m", label: "X", price };
}

test("normalizes prices to sum to 1", () => {
const outcomes = [makeOutcome(2), makeOutcome(3)];
normalizeBaoziOutcomes(outcomes);
expect(outcomes[0].price).toBeCloseTo(0.4);
expect(outcomes[1].price).toBeCloseTo(0.6);
});

test("does nothing when sum is zero", () => {
const outcomes = [makeOutcome(0), makeOutcome(0)];
normalizeBaoziOutcomes(outcomes);
expect(outcomes[0].price).toBe(0);
expect(outcomes[1].price).toBe(0);
});
});
16 changes: 16 additions & 0 deletions core/src/exchanges/baozi/price.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MarketOutcome } from "../../types";

export function clampBaoziPrice(value: number): number {
return Math.min(Math.max(value, 0), 1);
}

export function normalizeBaoziOutcomes(outcomes: MarketOutcome[]): void {
const sum = outcomes.reduce((acc, item) => acc + item.price, 0);
if (sum <= 0) {
return;
}

for (const outcome of outcomes) {
outcome.price = outcome.price / sum;
}
}
14 changes: 5 additions & 9 deletions core/src/exchanges/baozi/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import bs58 from 'bs58';
import { createHash } from 'crypto';
import { UnifiedMarket, MarketOutcome } from '../../types';
import { addBinaryOutcomes } from '../../utils/market-utils';
import { clampBaoziPrice, normalizeBaoziOutcomes } from './price';

// ---------------------------------------------------------------------------
// Constants
Expand Down Expand Up @@ -387,13 +388,13 @@ export function mapBooleanToUnified(market: BaoziMarket, pubkey: string): Unifie
outcomeId: `${pubkey}-YES`,
marketId: pubkey,
label: 'Yes',
price: yesPrice,
price: clampBaoziPrice(yesPrice),
},
{
outcomeId: `${pubkey}-NO`,
marketId: pubkey,
label: 'No',
price: noPrice,
price: clampBaoziPrice(noPrice),
},
];

Expand Down Expand Up @@ -431,17 +432,12 @@ export function mapRaceToUnified(market: BaoziRaceMarket, pubkey: string): Unifi
outcomeId: `${pubkey}-${i}`,
marketId: pubkey,
label: market.outcomeLabels[i] || `Outcome ${i + 1}`,
price: Math.min(Math.max(price, 0), 1),
price: clampBaoziPrice(price),
});
}

// Normalize prices to sum to 1
const priceSum = outcomes.reduce((s, o) => s + o.price, 0);
if (priceSum > 0) {
for (const o of outcomes) {
o.price = o.price / priceSum;
}
}
normalizeBaoziOutcomes(outcomes);

const um: UnifiedMarket = {
marketId: pubkey,
Expand Down
9 changes: 5 additions & 4 deletions core/src/exchanges/kalshi/fetchOHLCV.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { PriceCandle } from "../../types";
import { mapIntervalToKalshi } from "./utils";
import { validateIdFormat } from "../../utils/validation";
import { kalshiErrorMapper } from "./errors";
import { fromKalshiCents } from "./price";

export async function fetchOHLCV(
id: string,
Expand Down Expand Up @@ -98,10 +99,10 @@ export async function fetchOHLCV(

return {
timestamp: c.end_period_ts * 1000,
open: getVal("open") / 100,
high: getVal("high") / 100,
low: getVal("low") / 100,
close: getVal("close") / 100,
open: fromKalshiCents(getVal("open")),
high: fromKalshiCents(getVal("high")),
low: fromKalshiCents(getVal("low")),
close: fromKalshiCents(getVal("close")),
volume: c.volume || 0,
};
});
Expand Down
9 changes: 5 additions & 4 deletions core/src/exchanges/kalshi/fetchOrderBook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { OrderBook } from "../../types";
import { validateIdFormat } from "../../utils/validation";
import { kalshiErrorMapper } from "./errors";
import { getMarketsUrl } from "./config";
import { fromKalshiCents, invertKalshiCents } from "./price";

export async function fetchOrderBook(
baseUrl: string,
Expand Down Expand Up @@ -31,25 +32,25 @@ export async function fetchOrderBook(
// - Bids: people buying NO (use data.no directly)
// - Asks: people selling NO = people buying YES (invert data.yes)
bids = (data.no || []).map((level: number[]) => ({
price: level[0] / 100,
price: fromKalshiCents(level[0]),
size: level[1],
}));

asks = (data.yes || []).map((level: number[]) => ({
price: 1 - level[0] / 100, // Invert YES price to get NO ask price
price: invertKalshiCents(level[0]), // Invert YES price to get NO ask price
size: level[1],
}));
} else {
// YES outcome order book:
// - Bids: people buying YES (use data.yes directly)
// - Asks: people selling YES = people buying NO (invert data.no)
bids = (data.yes || []).map((level: number[]) => ({
price: level[0] / 100,
price: fromKalshiCents(level[0]),
size: level[1],
}));

asks = (data.no || []).map((level: number[]) => ({
price: 1 - level[0] / 100, // Invert NO price to get YES ask price
price: invertKalshiCents(level[0]), // Invert NO price to get YES ask price
size: level[1],
}));
}
Expand Down
3 changes: 2 additions & 1 deletion core/src/exchanges/kalshi/fetchTrades.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { HistoryFilterParams, TradesParams } from "../../BaseExchange";
import { Trade } from "../../types";
import { kalshiErrorMapper } from "./errors";
import { getMarketsUrl } from "./config";
import { fromKalshiCents } from "./price";

export async function fetchTrades(
baseUrl: string,
Expand All @@ -23,7 +24,7 @@ export async function fetchTrades(
return trades.map((t: any) => ({
id: t.trade_id,
timestamp: new Date(t.created_time).getTime(),
price: t.yes_price / 100,
price: fromKalshiCents(t.yes_price),
amount: t.count,
side: t.taker_side === "yes" ? "buy" : "sell",
}));
Expand Down
13 changes: 7 additions & 6 deletions core/src/exchanges/kalshi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { AuthenticationError } from "../../errors";
import { parseOpenApiSpec } from "../../utils/openapi";
import { kalshiApiSpec } from "./api";
import { getKalshiConfig, KalshiApiConfig, KALSHI_PATHS } from "./config";
import { fromKalshiCents, invertKalshiCents } from "./price";

// Re-export for external use
export type { KalshiWebSocketConfig };
Expand Down Expand Up @@ -176,20 +177,20 @@ export class KalshiExchange extends PredictionMarketExchange {

if (isNoOutcome) {
bids = (data.no || []).map((level: number[]) => ({
price: level[0] / 100,
price: fromKalshiCents(level[0]),
size: level[1],
}));
asks = (data.yes || []).map((level: number[]) => ({
price: 1 - level[0] / 100,
price: invertKalshiCents(level[0]),
size: level[1],
}));
} else {
bids = (data.yes || []).map((level: number[]) => ({
price: level[0] / 100,
price: fromKalshiCents(level[0]),
size: level[1],
}));
asks = (data.no || []).map((level: number[]) => ({
price: 1 - level[0] / 100,
price: invertKalshiCents(level[0]),
size: level[1],
}));
}
Expand Down Expand Up @@ -219,7 +220,7 @@ export class KalshiExchange extends PredictionMarketExchange {
return trades.map((t: any) => ({
id: t.trade_id,
timestamp: new Date(t.created_time).getTime(),
price: t.yes_price / 100,
price: fromKalshiCents(t.yes_price),
amount: t.count,
side: t.taker_side === "yes" ? "buy" : "sell",
}));
Expand Down Expand Up @@ -326,7 +327,7 @@ export class KalshiExchange extends PredictionMarketExchange {
return (data.fills || []).map((f: any) => ({
id: f.fill_id,
timestamp: new Date(f.created_time).getTime(),
price: f.yes_price / 100,
price: fromKalshiCents(f.yes_price),
amount: f.count,
side: f.side === "yes" ? ("buy" as const) : ("sell" as const),
orderId: f.order_id,
Expand Down
25 changes: 25 additions & 0 deletions core/src/exchanges/kalshi/price.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { fromKalshiCents, invertKalshiCents, invertKalshiUnified } from "./price";

describe("fromKalshiCents", () => {
test("converts cents to a decimal probability", () => {
expect(fromKalshiCents(55)).toBe(0.55);
expect(fromKalshiCents(0)).toBe(0);
expect(fromKalshiCents(100)).toBe(1);
});
});

describe("invertKalshiCents", () => {
test("returns the complement of a cent value", () => {
expect(invertKalshiCents(45)).toBeCloseTo(0.55);
expect(invertKalshiCents(0)).toBe(1);
expect(invertKalshiCents(100)).toBe(0);
});
});

describe("invertKalshiUnified", () => {
test("returns the complement of a normalized price", () => {
expect(invertKalshiUnified(0.45)).toBeCloseTo(0.55);
expect(invertKalshiUnified(0)).toBe(1);
expect(invertKalshiUnified(1)).toBe(0);
});
});
11 changes: 11 additions & 0 deletions core/src/exchanges/kalshi/price.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function fromKalshiCents(priceInCents: number): number {
return priceInCents / 100;
}

export function invertKalshiCents(priceInCents: number): number {
return 1 - priceInCents / 100;
}

export function invertKalshiUnified(price: number): number {
return 1 - price;
}
9 changes: 5 additions & 4 deletions core/src/exchanges/kalshi/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { UnifiedMarket, MarketOutcome, CandleInterval } from "../../types";
import { addBinaryOutcomes } from "../../utils/market-utils";
import { fromKalshiCents, invertKalshiUnified } from "./price";

export function mapMarketToUnified(
event: any,
Expand All @@ -10,11 +11,11 @@ export function mapMarketToUnified(
// Calculate price
let price = 0.5;
if (market.last_price) {
price = market.last_price / 100;
price = fromKalshiCents(market.last_price);
} else if (market.yes_ask && market.yes_bid) {
price = (market.yes_ask + market.yes_bid) / 200;
price = (fromKalshiCents(market.yes_ask) + fromKalshiCents(market.yes_bid)) / 2;
} else if (market.yes_ask) {
price = market.yes_ask / 100;
price = fromKalshiCents(market.yes_ask);
}

// Extract candidate name
Expand Down Expand Up @@ -44,7 +45,7 @@ export function mapMarketToUnified(
outcomeId: `${market.ticker}-NO`,
marketId: market.ticker,
label: candidateName ? `Not ${candidateName}` : "No",
price: 1 - price,
price: invertKalshiUnified(price),
priceChange24h: -priceChange, // Inverse change for No? simplified assumption
},
];
Expand Down
7 changes: 4 additions & 3 deletions core/src/exchanges/myriad/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { AuthenticationError } from '../../errors';
import { BASE_URL } from './utils';
import { parseOpenApiSpec } from '../../utils/openapi';
import { myriadApiSpec } from './api';
import { resolveMyriadPrice } from './price';

export class MyriadExchange extends PredictionMarketExchange {
override readonly has = {
Expand Down Expand Up @@ -140,7 +141,7 @@ export class MyriadExchange extends PredictionMarketExchange {
return filtered.map((t: any, index: number) => ({
id: `${t.blockNumber || t.timestamp}-${index}`,
timestamp: (t.timestamp || 0) * 1000,
price: t.shares > 0 ? Number(t.value) / Number(t.shares) : 0,
price: resolveMyriadPrice(t),
amount: Number(t.shares || 0),
side: t.action === 'buy' ? 'buy' as const : 'sell' as const,
}));
Expand Down Expand Up @@ -169,7 +170,7 @@ export class MyriadExchange extends PredictionMarketExchange {
return tradeEvents.map((t: any, i: number) => ({
id: `${t.blockNumber || t.timestamp}-${i}`,
timestamp: (t.timestamp || 0) * 1000,
price: t.shares > 0 ? Number(t.value) / Number(t.shares) : 0,
price: resolveMyriadPrice(t),
amount: Number(t.shares || 0),
side: t.action === 'buy' ? 'buy' as const : 'sell' as const,
}));
Expand Down Expand Up @@ -256,7 +257,7 @@ export class MyriadExchange extends PredictionMarketExchange {
outcomeLabel: pos.outcomeTitle || `Outcome ${pos.outcomeId}`,
size: Number(pos.shares || 0),
entryPrice: Number(pos.price || 0),
currentPrice: Number(pos.value || 0) / Math.max(Number(pos.shares || 1), 1),
currentPrice: resolveMyriadPrice(pos),
unrealizedPnL: Number(pos.profit || 0),
}));
}
Expand Down
19 changes: 19 additions & 0 deletions core/src/exchanges/myriad/price.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { resolveMyriadPrice } from "./price";

describe("resolveMyriadPrice", () => {
test("divides value by shares", () => {
expect(resolveMyriadPrice({ value: 100, shares: 4 })).toBe(25);
});

test("treats missing shares as 1", () => {
expect(resolveMyriadPrice({ value: 50 })).toBe(50);
});

test("treats zero shares as 1 to avoid division by zero", () => {
expect(resolveMyriadPrice({ value: 80, shares: 0 })).toBe(80);
});

test("treats missing value as 0", () => {
expect(resolveMyriadPrice({ shares: 5 })).toBe(0);
});
});
4 changes: 4 additions & 0 deletions core/src/exchanges/myriad/price.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function resolveMyriadPrice(event: any): number {
const shares = Math.max(Number(event.shares || 1), 1);
return Number(event.value || 0) / shares;
}
Loading