|
@@ -20,6 +20,7 @@ import {
|
|
|
import {
|
|
import {
|
|
|
scanWeatherMarkets,
|
|
scanWeatherMarkets,
|
|
|
groupMarketsByEvent,
|
|
groupMarketsByEvent,
|
|
|
|
|
+ CLOBReadClient,
|
|
|
} from '@poly-weather/market-engine';
|
|
} from '@poly-weather/market-engine';
|
|
|
import type { OrderManager } from '@poly-weather/market-engine';
|
|
import type { OrderManager } from '@poly-weather/market-engine';
|
|
|
import {
|
|
import {
|
|
@@ -47,6 +48,7 @@ export class TradingPipeline {
|
|
|
private riskManager: RiskManager;
|
|
private riskManager: RiskManager;
|
|
|
private portfolio: PortfolioTracker;
|
|
private portfolio: PortfolioTracker;
|
|
|
private orderManager: OrderManager;
|
|
private orderManager: OrderManager;
|
|
|
|
|
+ private clobReader: CLOBReadClient;
|
|
|
private redis: Redis | null;
|
|
private redis: Redis | null;
|
|
|
private cities: CityConfig[];
|
|
private cities: CityConfig[];
|
|
|
private dryRun: boolean;
|
|
private dryRun: boolean;
|
|
@@ -60,6 +62,7 @@ export class TradingPipeline {
|
|
|
autoExecute: boolean = true,
|
|
autoExecute: boolean = true,
|
|
|
) {
|
|
) {
|
|
|
this.orderManager = orderManager;
|
|
this.orderManager = orderManager;
|
|
|
|
|
+ this.clobReader = new CLOBReadClient();
|
|
|
this.cities = cities;
|
|
this.cities = cities;
|
|
|
this.dryRun = dryRun;
|
|
this.dryRun = dryRun;
|
|
|
this._autoExecute = autoExecute;
|
|
this._autoExecute = autoExecute;
|
|
@@ -219,10 +222,24 @@ export class TradingPipeline {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- result.signalsGenerated += allSignals.length;
|
|
|
|
|
|
|
+ // Refine signals with live CLOB best-ask prices
|
|
|
|
|
+ const refinedSignals: Signal[] = [];
|
|
|
|
|
+ for (const signal of allSignals) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const refined = await this.refineSignalWithCLOB(signal, market);
|
|
|
|
|
+ if (refined) {
|
|
|
|
|
+ refinedSignals.push(refined);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ logger.warn({ type: signal.type, city: signal.city, err }, 'CLOB price refinement failed, using Gamma price');
|
|
|
|
|
+ refinedSignals.push(signal);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ result.signalsGenerated += refinedSignals.length;
|
|
|
|
|
|
|
|
// Persist and optionally execute signals
|
|
// Persist and optionally execute signals
|
|
|
- for (const signal of allSignals) {
|
|
|
|
|
|
|
+ for (const signal of refinedSignals) {
|
|
|
// Persist signal to DB first
|
|
// Persist signal to DB first
|
|
|
const signalId = await persistSignal(signal);
|
|
const signalId = await persistSignal(signal);
|
|
|
|
|
|
|
@@ -253,6 +270,156 @@ export class TradingPipeline {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Refine a signal's edge using the live CLOB orderbook best-ask price
|
|
|
|
|
+ * instead of the Gamma API midpoint snapshot.
|
|
|
|
|
+ *
|
|
|
|
|
+ * For TAIL_NO: look up the NO token's best ask from CLOB.
|
|
|
|
|
+ * edge = (1 - noBestAsk) - modelProbability
|
|
|
|
|
+ * expectedReturn = edge / noBestAsk
|
|
|
|
|
+ * If edge drops below threshold after refinement, discard the signal.
|
|
|
|
|
+ *
|
|
|
|
|
+ * For LATENCY_ARB / PARITY_ARB: pass through unchanged (they use
|
|
|
|
|
+ * different pricing logic).
|
|
|
|
|
+ */
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Fetch CLOB best ask for a given token.
|
|
|
|
|
+ * Returns the lowest ask price, or null if orderbook is empty.
|
|
|
|
|
+ */
|
|
|
|
|
+ private async getCLOBBestAsk(tokenId: string): Promise<number | null> {
|
|
|
|
|
+ const book = await this.clobReader.getOrderBook(tokenId);
|
|
|
|
|
+ const asks = (book.asks || [])
|
|
|
|
|
+ .map((a) => parseFloat(a.price))
|
|
|
|
|
+ .sort((a, b) => a - b);
|
|
|
|
|
+ return asks.length > 0 ? asks[0] : null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Refine a signal's price and edge using the live CLOB orderbook best-ask
|
|
|
|
|
+ * instead of the Gamma API midpoint snapshot.
|
|
|
|
|
+ *
|
|
|
|
|
+ * For TAIL_NO (BUY NO):
|
|
|
|
|
+ * noBestAsk from CLOB → implied YES = 1 - noBestAsk
|
|
|
|
|
+ * edge = impliedYes - modelProb
|
|
|
|
|
+ *
|
|
|
|
|
+ * For LATENCY_ARB:
|
|
|
|
|
+ * Determine actual executable price from CLOB best ask on the relevant side.
|
|
|
|
|
+ * Recalculate marketLag with real price. Drop if lag < threshold.
|
|
|
|
|
+ *
|
|
|
|
|
+ * For PARITY_ARB: pass through (uses aggregate pricing across all buckets).
|
|
|
|
|
+ */
|
|
|
|
|
+ private async refineSignalWithCLOB(
|
|
|
|
|
+ signal: Signal,
|
|
|
|
|
+ market: ParsedWeatherMarket,
|
|
|
|
|
+ ): Promise<Signal | null> {
|
|
|
|
|
+ if (signal.type === 'PARITY_ARB') {
|
|
|
|
|
+ return signal;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const bucket = market.buckets.find((b) => b.label === signal.bucketLabel);
|
|
|
|
|
+ if (!bucket) {
|
|
|
|
|
+ return signal;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (signal.type === 'TAIL_NO') {
|
|
|
|
|
+ if (!bucket.noTokenId) {
|
|
|
|
|
+ logger.debug({ bucket: signal.bucketLabel }, 'No NO token ID — skipping CLOB refinement');
|
|
|
|
|
+ return signal;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const noBestAsk = await this.getCLOBBestAsk(bucket.noTokenId);
|
|
|
|
|
+ if (noBestAsk === null) {
|
|
|
|
|
+ logger.debug({ bucket: signal.bucketLabel }, 'NO orderbook empty — using Gamma price');
|
|
|
|
|
+ return signal;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const impliedYesPrice = 1 - noBestAsk;
|
|
|
|
|
+ const edge = impliedYesPrice - signal.modelProbability;
|
|
|
|
|
+ const expectedReturn = edge / noBestAsk;
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ {
|
|
|
|
|
+ city: signal.city,
|
|
|
|
|
+ bucket: signal.bucketLabel,
|
|
|
|
|
+ gammaNO: signal.noPrice.toFixed(3),
|
|
|
|
|
+ clobNO: noBestAsk.toFixed(3),
|
|
|
|
|
+ gammaEdge: signal.edge.toFixed(3),
|
|
|
|
|
+ clobEdge: edge.toFixed(3),
|
|
|
|
|
+ },
|
|
|
|
|
+ 'CLOB price refinement (TAIL_NO)',
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (edge < 0.02) {
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ { bucket: signal.bucketLabel, edge: edge.toFixed(4) },
|
|
|
|
|
+ 'Signal dropped: edge too small after CLOB refinement',
|
|
|
|
|
+ );
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...signal,
|
|
|
|
|
+ marketYesPrice: impliedYesPrice,
|
|
|
|
|
+ noPrice: noBestAsk,
|
|
|
|
|
+ edge,
|
|
|
|
|
+ expectedReturn,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // LATENCY_ARB — refine currentPrice and marketLag with CLOB best ask
|
|
|
|
|
+ if (signal.type === 'LATENCY_ARB') {
|
|
|
|
|
+ // Determine which side's best ask matters:
|
|
|
|
|
+ // BUY_YES → need YES best ask (cost to buy YES)
|
|
|
|
|
+ // BUY_NO → need NO best ask, implied YES = 1 - noBestAsk
|
|
|
|
|
+ let clobYesPrice: number | null = null;
|
|
|
|
|
+
|
|
|
|
|
+ if (signal.side === 'BUY_YES' && bucket.yesTokenId) {
|
|
|
|
|
+ clobYesPrice = await this.getCLOBBestAsk(bucket.yesTokenId);
|
|
|
|
|
+ } else if (signal.side === 'BUY_NO' && bucket.noTokenId) {
|
|
|
|
|
+ const noBestAsk = await this.getCLOBBestAsk(bucket.noTokenId);
|
|
|
|
|
+ if (noBestAsk !== null) {
|
|
|
|
|
+ clobYesPrice = 1 - noBestAsk;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (clobYesPrice === null) {
|
|
|
|
|
+ logger.debug({ bucket: signal.bucketLabel }, 'CLOB price unavailable for latency arb');
|
|
|
|
|
+ return signal;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const marketLag = Math.abs(clobYesPrice - signal.newModelProbability);
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ {
|
|
|
|
|
+ city: signal.city,
|
|
|
|
|
+ bucket: signal.bucketLabel,
|
|
|
|
|
+ side: signal.side,
|
|
|
|
|
+ gammaPrice: signal.currentPrice.toFixed(3),
|
|
|
|
|
+ clobPrice: clobYesPrice.toFixed(3),
|
|
|
|
|
+ gammaLag: Math.abs(signal.currentPrice - signal.newModelProbability).toFixed(3),
|
|
|
|
|
+ clobLag: marketLag.toFixed(3),
|
|
|
|
|
+ },
|
|
|
|
|
+ 'CLOB price refinement (LATENCY_ARB)',
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // If market already repriced (lag < 8%), signal is stale
|
|
|
|
|
+ if (marketLag < 0.08) {
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ { bucket: signal.bucketLabel, marketLag: marketLag.toFixed(4) },
|
|
|
|
|
+ 'Signal dropped: market already repriced after CLOB check',
|
|
|
|
|
+ );
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...signal,
|
|
|
|
|
+ currentPrice: clobYesPrice,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return signal;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Get portfolio summary.
|
|
* Get portfolio summary.
|
|
|
*/
|
|
*/
|