fix: remove cut-loss from maker strategy — hold to resolution
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
23bb74127c
commit
9a89efb071
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user