Skip to content

Bullish integration#65

Merged
thaaddeus merged 18 commits into
masterfrom
bullish-integration
May 5, 2026
Merged

Bullish integration#65
thaaddeus merged 18 commits into
masterfrom
bullish-integration

Conversation

@marcinvalas
Copy link
Copy Markdown
Contributor

No description provided.

@marcinvalas marcinvalas requested a review from thaaddeus May 4, 2026 15:08
@marcinvalas marcinvalas marked this pull request as ready for review May 4, 2026 15:09
@thaaddeus thaaddeus dismissed their stale review May 4, 2026 17:12

Retracted.

@thaaddeus
Copy link
Copy Markdown
Member

Sorry, ignore previous comments. Those were no approved.

Comment thread src/mappers/bullish.ts Outdated

export class BullishTradesMapper implements Mapper<'bullish', Trade> {
canHandle(message: BullishMessage): message is BullishAnonymousTradeUpdateMessage {
return message.dataType === 'V1TAAnonymousTradeUpdate' && (message.type === 'snapshot' || message.type === 'update')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to skip snapshots (old not real-time trades)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread src/mappers/bullish.ts Outdated
}

private mapLevels(levels: string[]): BookPriceLevel[] {
return levels.reduce<BookPriceLevel[]>((result, value, index) => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

depending on the perf characteristics, but if below code is meaningfully faster I'd use it instead even if it's uglier, but it's perf is +-10% the same it's fine to keep the reduce:

const result = new Array<BookPriceLevel>(levels.length / 2)
for (let i = 0, j = 0; i < levels.length; i += 2, j++) {
  result[j] = { price: Number(levels[i]), amount: Number(levels[i + 1]) }
}
return result

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread src/mappers/bullish.ts Outdated

export class BullishBookChangeMapper implements Mapper<'bullish', BookChange> {
canHandle(message: BullishMessage): message is BullishLevel2Message {
return message.dataType === 'V1TALevel2' && (message.type === 'snapshot' || message.type === 'update')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to check snapshot and update type here? are other values possible in the exchange contract?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread src/mappers/bullish.ts Outdated
const timestamp = new Date(message.data.updatedAtDatetime)
this.indexPrices.set(message.data.assetSymbol, { price, timestamp })

for (const symbol of this.derivativeSymbolsByIndexAsset.get(message.data.assetSymbol) ?? []) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

V1TAIndexPrice should only update cached index price state, and not emit ticker message, same for options

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread src/mappers/bullish.ts Outdated
id: trade.tradeId,
price: Number(trade.price),
amount: Number(trade.quantity),
side: trade.side.toLowerCase() as 'buy' | 'sell',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bullish side should not be copied directly into Tardis trade.side.

Bullish exposes two separate fields on trades:

  • side — the side of the order represented by the trade row
  • isTaker — whether that represented order was the taker

Tardis has a different normalized contract: trade.side is the taker/aggressor side. That means the mapper must answer “did the taker buy or sell?”, not just “what side did Bullish put in side?”

{
  "symbol": "AUSDUSDC",
  "tradeId": "100120000008559857",
  "price": "1.0000",
  "quantity": "1.92651930",
  "side": "SELL",
  "isTaker": false,
  "createdAtDatetime": "2026-05-04T17:23:16.671Z"
}

If we copy side: "SELL" directly, Tardis emits:

{ "side": "sell" }

But isTaker: false means the represented SELL order was the maker/resting side. The taker was on the opposite side, so the normalized Tardis trade side should be:

{ "side": "buy" }

The mapping rule should therefore be:

function mapBullishTradeSide(side: 'BUY' | 'SELL', isTaker: boolean): 'buy' | 'sell' {
  if (isTaker) {
    return side === 'BUY' ? 'buy' : 'sell'
  }

  return side === 'BUY' ? 'sell' : 'buy'
}

So if we ignore isTaker, trades where Bullish reports the maker side will be normalized with the wrong aggressor direction.

Tests should cover ideally:

side=BUY,  isTaker=true  -> buy
side=SELL, isTaker=true  -> sell
side=BUY,  isTaker=false -> sell
side=SELL, isTaker=false -> buy

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i understand, thanks, done

Comment thread src/mappers/bullish.ts Outdated
pendingTickerInfo.updateLastPrice(asNumberIfValid(message.data.last))
pendingTickerInfo.updateMarkPrice(asNumberIfValid(message.data.markPrice))
pendingTickerInfo.updateFundingRate(asNumberIfValid(message.data.fundingRate))
pendingTickerInfo.updateOpenInterest(asNumberIfValid(message.data.openInterest))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use asNumberIfValid where 0 is a valid Bullish ticker value

asNumberIfValid returns undefined for zero. That drops real Bullish values from normalized output.
In V1TATickerResponse messages, zero appears as a valid value for:

  • openInterest
  • fundingRate
  • option markPrice
  • option greeks: delta, gamma, theta, vega

Example:

{
  "symbol": "BTC-USDC-20260515-150000-C",
  "markPrice": "0.0000",
  "openInterest": "0.00000000",
  "delta": "0.00000000",
  "gamma": "0.00000000",
  "theta": "0.0000",
  "vega": "0.0000",
  "impliedVolatility": "0.6182"
}

These values should normalize to 0, not undefined.

This is risky for derivative tickers because PendingTickerInfoHelper ignores undefined updates. If a field changes from non-zero to zero, parsing zero as undefined can leave the previous non-zero value cached and emit stale data.

Use a zero-preserving parser for:

  • derivative ticker fundingRate
  • derivative ticker openInterest
  • option summary openInterest
  • option summary markPrice
  • option summary delta
  • option summary gamma
  • option summary theta
  • option summary vega

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@marcinvalas marcinvalas requested a review from thaaddeus May 5, 2026 11:00
Comment thread src/mappers/bullish.ts Outdated
Comment thread src/mappers/bullish.ts Outdated
@thaaddeus thaaddeus merged commit c148ea0 into master May 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants