From 9a89efb071c547259c0995b45a5fbf97ce785901 Mon Sep 17 00:00:00 2001 From: direkturcrypto Date: Fri, 6 Mar 2026 16:59:58 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20remove=20cut-loss=20from=20maker=20strat?= =?UTF-8?q?egy=20=E2=80=94=20hold=20to=20resolution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No cut-loss needed: worst case is losing buy cost (2c/share) which resolves on-chain. If on winning side, payout is $1/share. Removed marketSell, cutLossSells, and makerCutLossTime config. Co-Authored-By: Claude Opus 4.6 --- .env.example | 3 - src/config/index.js | 1 - src/maker-bot.js | 4 +- src/maker.js | 4 +- src/services/makerExecutor.js | 113 +++++++++------------------------- 5 files changed, 32 insertions(+), 93 deletions(-) diff --git a/.env.example b/.env.example index 5b0db78..a7ccecf 100644 --- a/.env.example +++ b/.env.example @@ -181,9 +181,6 @@ MAKER_SELL_PRICE=0.03 # Shares per side (e.g. 50 shares × 0.02 = $1.00 cost per side) MAKER_TRADE_SIZE=50 -# Seconds before market close to trigger cut-loss -MAKER_CUT_LOSS_TIME=60 - # How often to poll for new markets (seconds) MAKER_POLL_INTERVAL=10 diff --git a/src/config/index.js b/src/config/index.js index 9d3b562..eebd89b 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -112,7 +112,6 @@ const config = { makerBuyPrice: parseFloat(process.env.MAKER_BUY_PRICE || '0.02'), makerSellPrice: parseFloat(process.env.MAKER_SELL_PRICE || '0.03'), makerTradeSize: parseFloat(process.env.MAKER_TRADE_SIZE || '50'), // shares per side - makerCutLossTime: parseInt(process.env.MAKER_CUT_LOSS_TIME || '60', 10), makerPollInterval: parseInt(process.env.MAKER_POLL_INTERVAL || '10', 10) * 1000, makerMonitorMs: parseInt(process.env.MAKER_MONITOR_MS || '2000', 10), // how fast to poll order status diff --git a/src/maker-bot.js b/src/maker-bot.js index 6d9dfc7..159f858 100644 --- a/src/maker-bot.js +++ b/src/maker-bot.js @@ -107,7 +107,7 @@ async function runStrategy(market) { pendingByAsset.delete(market.asset); const secsLeft = Math.round((new Date(queued.endTime).getTime() - Date.now()) / 1000); - if (secsLeft > config.makerCutLossTime) { + if (secsLeft > 30) { logger.success(`MAKER[${market.asset?.toUpperCase()}]: executing queued market (${secsLeft}s left)`); runStrategy(queued); } else { @@ -160,7 +160,7 @@ logger.info(`Sell @ : $${config.makerSellPrice} per share`); logger.info(`Size : ${config.makerTradeSize} shares/side`); logger.info(`Cost/side : $${costPerSide.toFixed(2)}`); logger.info(`Profit : $${profitPerCycle.toFixed(2)} per cycle`); -logger.info(`Cut loss : ${config.makerCutLossTime}s before close`); +logger.info(`Cut loss : NONE (hold to resolution)`); logger.info('=========================================='); startMakerDetector(handleNewMarket); diff --git a/src/maker.js b/src/maker.js index 19a53f9..c0d2877 100644 --- a/src/maker.js +++ b/src/maker.js @@ -78,7 +78,7 @@ async function buildStatusContent() { lines.push(` Size : ${config.makerTradeSize} shares/side`); lines.push(` Cost/side: $${(config.makerTradeSize * config.makerBuyPrice).toFixed(2)}`); lines.push(` Profit : $${((config.makerSellPrice - config.makerBuyPrice) * config.makerTradeSize).toFixed(2)}/cycle`); - lines.push(` Cut loss : ${config.makerCutLossTime}s before close`); + lines.push(` No CL : hold to resolution if sell unfilled`); lines.push(''); // Active positions @@ -204,7 +204,7 @@ async function runStrategy(market) { const endMs = new Date(queued.endTime).getTime(); const secsLeft = Math.round((endMs - Date.now()) / 1000); - if (secsLeft > config.makerCutLossTime) { + if (secsLeft > 30) { logger.success(`MAKER[${market.asset?.toUpperCase()}]: executing queued market (${secsLeft}s left)`); runStrategy(queued); } else { diff --git a/src/services/makerExecutor.js b/src/services/makerExecutor.js index eb007e5..7930f15 100644 --- a/src/services/makerExecutor.js +++ b/src/services/makerExecutor.js @@ -1,6 +1,6 @@ /** * makerExecutor.js - * Buy Low, Sell High Market Maker — no splitPosition. + * Buy Low, Sell High Market Maker — no splitPosition, no cut-loss. * * Flow: * 1. Place concurrent limit BUY on UP + DOWN at makerBuyPrice (e.g. 2c) @@ -9,8 +9,9 @@ * a. Immediately place limit SELL for filled shares at makerSellPrice (e.g. 3c) * b. Cancel the other side's buy order * 4. Partial fills → partial sells placed immediately - * 5. Monitor sell orders until all filled or cut-loss - * 6. Cut-loss: market sell remaining tokens + * 5. Monitor sell orders until filled or market expires + * 6. No cut-loss — worst case is losing buy cost (2c/share) on wrong side, + * or gaining $1/share if on winning side and sell doesn't fill */ import { Side, OrderType } from '@polymarket/clob-client'; @@ -78,31 +79,6 @@ async function cancelOrder(orderId) { } } -async function marketSell(tokenId, shares, tickSize, negRisk) { - if (config.dryRun) { - try { - const mp = await getClient().getMidpoint(tokenId); - const price = parseFloat(mp?.mid ?? mp ?? '0') || 0; - return { success: true, fillPrice: price }; - } catch { - return { success: true, fillPrice: 0 }; - } - } - const client = getClient(); - try { - const res = await client.createAndPostMarketOrder( - { tokenID: tokenId, side: Side.SELL, amount: shares, price: 0.01 }, - { tickSize, negRisk }, - OrderType.FOK, - ); - if (!res?.success) return { success: false, fillPrice: 0 }; - return { success: true, fillPrice: parseFloat(res.price || '0') }; - } catch (err) { - logger.error('MAKER market sell error:', err.message); - return { success: false, fillPrice: 0 }; - } -} - // ── Order status ────────────────────────────────────────────────────────────── async function getOrderFill(orderId) { @@ -197,7 +173,7 @@ export async function executeMakerStrategy(market) { activePositions.set(conditionId, pos); - // ── 3. Monitor buy orders (concurrent multi-thread style) ──── + // ── 3. Monitor buy → sell (concurrent) ─────────────────────── try { await monitorBuyPhase(pos, tag, sim); await monitorSellPhase(pos, tag, sim); @@ -208,7 +184,16 @@ export async function executeMakerStrategy(market) { // ── Final P&L ──────────────────────────────────────────────── const pnl = pos.totalRevenue - pos.totalCost; const sign = pnl >= 0 ? '+' : ''; - logger.money(`MAKER${tag}: ${sim}strategy complete | cost $${pos.totalCost.toFixed(4)} | revenue $${pos.totalRevenue.toFixed(4)} | P&L ${sign}$${pnl.toFixed(4)}`); + + if (pos.status === 'expired-holding') { + // Position held to expiry — will resolve on-chain + const side = pos[pos.winner]; + logger.info(`MAKER${tag}: ${sim}holding ${side.buyFilled.toFixed(2)} ${pos.winner.toUpperCase()} shares to resolution`); + logger.info(`MAKER${tag}: ${sim}if winning side → payout $${side.buyFilled.toFixed(2)} (cost $${pos.totalCost.toFixed(4)})`); + logger.info(`MAKER${tag}: ${sim}if losing side → payout $0 (loss $${pos.totalCost.toFixed(4)})`); + } else { + logger.money(`MAKER${tag}: ${sim}strategy complete | cost $${pos.totalCost.toFixed(4)} | revenue $${pos.totalRevenue.toFixed(4)} | P&L ${sign}$${pnl.toFixed(4)}`); + } activePositions.delete(conditionId); } @@ -216,7 +201,7 @@ export async function executeMakerStrategy(market) { // ── Buy phase: monitor both sides concurrently ─────────────────────────────── async function monitorBuyPhase(pos, tag, sim) { - const { makerBuyPrice, makerSellPrice, makerMonitorMs, makerCutLossTime } = config; + const { makerBuyPrice, makerSellPrice, makerMonitorMs } = config; // Run two concurrent monitors — first full fill wins const monitorSide = async (sideKey) => { @@ -229,11 +214,8 @@ async function monitorBuyPhase(pos, tag, sim) { while (!pos.winner) { const msLeft = new Date(pos.endTime).getTime() - Date.now(); - // Cut-loss check - if (msLeft <= makerCutLossTime * 1000) { - logger.warn(`MAKER${tag}: buy phase cut-loss (${Math.round(msLeft / 1000)}s left) — cancelling buy orders`); - break; - } + // Market expired — buy orders expire naturally + if (msLeft <= 0) break; // Check fill let fill; @@ -295,35 +277,27 @@ async function monitorBuyPhase(pos, tag, sim) { monitorSide('down'), ]); - // If no winner (cut-loss), cancel all remaining buy orders + // If no winner (market expired without fills) if (!pos.winner) { - pos.status = 'cut-buy'; - for (const key of ['up', 'down']) { - const side = pos[key]; - if (side.buyOrderId && !side.cancelled) { - await cancelOrder(side.buyOrderId); - side.cancelled = true; - } - } - // If any partial fills exist, still process sells + // Check if any partial fills exist const anyFill = pos.up.buyFilled > 0 || pos.down.buyFilled > 0; if (anyFill) { pos.winner = pos.up.buyFilled >= pos.down.buyFilled ? 'up' : 'down'; pos.status = 'selling'; - logger.warn(`MAKER${tag}: partial fill — selling ${pos.winner.toUpperCase()} ${pos[pos.winner].buyFilled.toFixed(2)} shares`); + logger.info(`MAKER${tag}: partial fill — monitoring sell for ${pos.winner.toUpperCase()} ${pos[pos.winner].buyFilled.toFixed(2)} shares`); } else { pos.status = 'done'; - logger.warn(`MAKER${tag}: no fills during buy phase — exiting with $0 loss`); + logger.info(`MAKER${tag}: no fills — buy orders expired naturally, $0 loss`); } } } -// ── Sell phase: monitor all sell orders ─────────────────────────────────────── +// ── Sell phase: monitor sell orders until filled or market expires ──────────── async function monitorSellPhase(pos, tag, sim) { - if (pos.status === 'done') return; // nothing to sell + if (pos.status === 'done') return; - const { makerSellPrice, makerMonitorMs, makerCutLossTime } = config; + const { makerSellPrice, makerMonitorMs } = config; const winnerKey = pos.winner; if (!winnerKey) return; @@ -335,13 +309,6 @@ async function monitorSellPhase(pos, tag, sim) { while (true) { const msLeft = new Date(pos.endTime).getTime() - Date.now(); - // Cut-loss: market sell remaining - if (msLeft <= makerCutLossTime * 1000) { - logger.warn(`MAKER${tag}: sell phase cut-loss (${Math.round(msLeft / 1000)}s left)`); - await cutLossSells(pos, side, sideName, tag, sim); - break; - } - // Check all sell orders concurrently const checks = await Promise.all( side.sellOrders.map(async (so) => { @@ -380,37 +347,13 @@ async function monitorSellPhase(pos, tag, sim) { break; } - // Market expired + // Market expired — position resolves on-chain (no cut-loss) if (msLeft <= 0) { - pos.status = 'expired'; - logger.warn(`MAKER${tag}: market expired`); + pos.status = 'expired-holding'; + logger.info(`MAKER${tag}: market expired — holding position to resolution (no cut-loss)`); break; } await sleep(makerMonitorMs); } } - -async function cutLossSells(pos, side, sideName, tag, sim) { - const { makerSellPrice } = config; - - // Cancel unfilled sell orders and market sell - let remainingShares = 0; - for (const so of side.sellOrders) { - if (!so.filled) { - await cancelOrder(so.orderId); - remainingShares += so.shares; - } - } - - if (remainingShares > 0) { - logger.warn(`MAKER${tag}: ${sim}market-selling ${remainingShares.toFixed(2)} ${sideName} shares`); - const result = await marketSell(side.tokenId, remainingShares, pos.tickSize, pos.negRisk); - if (result.success) { - pos.totalRevenue += remainingShares * result.fillPrice; - logger.warn(`MAKER${tag}: ${sim}${sideName} CL sold @ $${result.fillPrice.toFixed(3)}`); - } - } - - pos.status = 'done'; -}