From 3b302a671caa7f0743ac05ceb3da9ecee9331c7e Mon Sep 17 00:00:00 2001 From: multipleof4 Date: Mon, 16 Mar 2026 12:29:52 -0700 Subject: [PATCH] Feat: Split paper/live tracking, orderbook pricing --- lib/strategies/bull-dip-buyer.js | 70 +++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/lib/strategies/bull-dip-buyer.js b/lib/strategies/bull-dip-buyer.js index aa40699..30df40e 100644 --- a/lib/strategies/bull-dip-buyer.js +++ b/lib/strategies/bull-dip-buyer.js @@ -3,42 +3,84 @@ import { BaseStrategy } from './base.js'; export class BullDipBuyer extends BaseStrategy { constructor(config = {}) { super('bull-dip-buyer', { - maxYesPrice: config.maxYesPrice || 45, // Buy the dip when Yes is cheap - minYesPrice: config.minYesPrice || 15, // Avoid completely dead markets - betSize: config.betSize || 1, // Changed to $1 per Master's orders! - cooldownMs: config.cooldownMs || 60000, + maxYesPrice: config.maxYesPrice || 45, + minYesPrice: config.minYesPrice || 15, + betSize: config.betSize || 1, + slippage: config.slippage || 3, // willing to pay up to 3¢ above target + cooldownMs: config.cooldownMs || 20000, // 20s for fast markets ...config }); - - this.lastTradeTime = 0; - this.lastTradeTicker = null; + + // Separate tracking for paper vs live + this._lastTrade = { + paper: { time: 0, ticker: null }, + live: { time: 0, ticker: null } + }; } - evaluate(state) { + evaluate(state, caller = 'paper') { if (!state || !this.enabled) return null; + const track = this._lastTrade[caller] || this._lastTrade.paper; const now = Date.now(); - if (now - this.lastTradeTime < this.config.cooldownMs) return null; - if (state.ticker === this.lastTradeTicker) return null; + if (now - track.time < this.config.cooldownMs) return null; + if (state.ticker === track.ticker) return null; const { yesPct } = state; - // Only buy YES when it dips into our target buy zone if (yesPct <= this.config.maxYesPrice && yesPct >= this.config.minYesPrice) { + const maxPrice = Math.min(yesPct + this.config.slippage, 95); + + // Check orderbook for real liquidity if available + const bestAsk = this._getBestYesAsk(state); + let reason = `Bullish dip buy: Yes @ ${yesPct}¢`; + if (bestAsk != null) { + if (bestAsk > maxPrice) { + return null; // best ask too expensive even with slippage + } + reason = `Bullish dip buy: Yes @ ${yesPct}¢ (ask: ${bestAsk}¢)`; + } + const signal = { strategy: this.name, side: 'yes', price: yesPct, + maxPrice, size: this.config.betSize, - reason: `Bullish dip buy: Yes dropped to ${yesPct}¢`, + reason, ticker: state.ticker }; - this.lastTradeTime = now; - this.lastTradeTicker = state.ticker; + track.time = now; + track.ticker = state.ticker; return signal; } return null; } + + _getBestYesAsk(state) { + // Best yes ask = lowest price someone is selling yes at + // In Kalshi terms: best ask on yes side, OR 100 - best bid on no side + const ob = state.orderbook; + if (!ob) return null; + + // Yes side: sorted descending by price, ask = lowest offer + if (ob.yes?.length) { + // orderbook yes levels are bids (people wanting to buy yes) + // The ask is 100 - best_no_bid + } + if (ob.no?.length) { + const bestNoBid = ob.no[0]?.[0]; + if (bestNoBid != null) return 100 - bestNoBid; + } + return null; + } + + toJSON() { + return { + ...super.toJSON(), + lastTradeTicker: this._lastTrade.live.ticker || this._lastTrade.paper.ticker + }; + } }