From 0148b0bbefc3772843f6d07afe4627c2403321cb Mon Sep 17 00:00:00 2001 From: direkturcrypto Date: Wed, 25 Feb 2026 04:30:12 +0700 Subject: [PATCH] feat: GTC fallback when FAK finds no liquidity (next market copy trades) When copying a trader who buys into the next periodic market before sellers exist, FAK returns 0 fill each attempt. After exhausting FAK retries, the bot now falls back to a GTC limit order at price*1.02 and polls getOrder() every 3s until filled or GTC_FALLBACK_TIMEOUT (default 60s) expires. If the GTC times out it is cancelled. GTC_FALLBACK_TIMEOUT=0 disables the fallback entirely. Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 5 +++ src/config/index.js | 4 +++ src/services/executor.js | 71 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/.env.example b/.env.example index 10b5bce..dfaf184 100644 --- a/.env.example +++ b/.env.example @@ -68,6 +68,11 @@ REDEEM_INTERVAL=60 # Default 300 = skip if market ends in less than 5 minutes MIN_MARKET_TIME_LEFT=300 +# When FAK finds no liquidity (e.g. copying into "next market" before it opens), +# fall back to a GTC limit order and wait this many seconds for it to fill. +# Set to 0 to disable the GTC fallback entirely. +GTC_FALLBACK_TIMEOUT=60 + # ───────────────────────────────────────────── # DRY RUN (set true to simulate without real trades) # ───────────────────────────────────────────── diff --git a/src/config/index.js b/src/config/index.js index 75f247a..0149b50 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -49,6 +49,10 @@ const config = { // Skip buy if market closes within this many seconds (default 5 minutes) minMarketTimeLeft: parseInt(process.env.MIN_MARKET_TIME_LEFT || '300', 10), + // Seconds to wait for a GTC limit order to fill when FAK finds no liquidity + // (happens when copying trades into "next market" before sellers arrive) + gtcFallbackTimeout: parseInt(process.env.GTC_FALLBACK_TIMEOUT || '60', 10), + // ── Market Maker ────────────────────────────────────────────── mmAssets: (process.env.MM_ASSETS || 'btc') .split(',').map((s) => s.trim().toLowerCase()).filter(Boolean), diff --git a/src/services/executor.js b/src/services/executor.js index 9868972..e552ea1 100644 --- a/src/services/executor.js +++ b/src/services/executor.js @@ -115,6 +115,67 @@ export function executeBuy(trade) { return queued; } +/** + * GTC fallback for when FAK finds no liquidity (e.g. trader buys into "next market" + * before any sellers exist). Places a GTC limit order and polls until filled or timeout. + * + * Returns { sharesFilled, costFilled } on success, or null on failure/timeout. + */ +async function _tryGtcFallback(client, tokenId, tradeSize, price, marketOpts) { + const gtcPrice = parseFloat(Math.min(price * 1.02, 0.99).toFixed(4)); + const shares = parseFloat((tradeSize / gtcPrice).toFixed(4)); + + logger.info(`No liquidity via FAK — placing GTC limit buy: ${shares} shares @ $${gtcPrice}`); + + let orderId; + try { + const resp = await client.createAndPostOrder( + { tokenID: tokenId, side: Side.BUY, price: gtcPrice, size: shares }, + { tickSize: marketOpts.tickSize, negRisk: marketOpts.negRisk }, + OrderType.GTC, + ); + if (!resp?.success) { + logger.warn(`GTC fallback rejected: ${resp?.errorMsg || 'unknown'}`); + return null; + } + orderId = resp.orderID; + logger.info(`GTC order placed: ${orderId} — waiting for fill (up to ${config.gtcFallbackTimeout}s)...`); + } catch (err) { + logger.warn(`GTC fallback order failed: ${err.message}`); + return null; + } + + const deadline = Date.now() + config.gtcFallbackTimeout * 1000; + const pollMs = 3000; + + while (Date.now() < deadline) { + await new Promise((r) => setTimeout(r, pollMs)); + try { + const order = await client.getOrder(orderId); + const matched = parseFloat(order?.size_matched ?? order?.matched_amount ?? '0'); + const status = (order?.status ?? order?.order_status ?? '').toLowerCase(); + + if (matched > 0 || status === 'matched' || status === 'filled') { + const sharesFilled = matched > 0 ? matched : shares; + const costFilled = sharesFilled * gtcPrice; + logger.success(`GTC filled: ${sharesFilled.toFixed(4)} shares @ $${gtcPrice} | orderID: ${orderId}`); + return { sharesFilled, costFilled }; + } + + // Order gone from open orders also means it was matched + if (status === 'cancelled') { + logger.warn(`GTC order ${orderId} was cancelled externally`); + return null; + } + } catch { /* getOrder can 404 briefly — keep polling */ } + } + + // Timed out — cancel the GTC + logger.warn(`GTC order ${orderId} not filled in ${config.gtcFallbackTimeout}s — cancelling`); + try { await client.cancelOrder({ orderID: orderId }); } catch { /* ignore */ } + return null; +} + /** * Internal: the actual buy logic, guaranteed to run serially per market. */ @@ -262,6 +323,16 @@ async function _doExecuteBuy(trade, marketOpts, effectiveConditionId) { } } + // FAK found no liquidity — fall back to GTC limit order and wait for fill + if (!filled && config.gtcFallbackTimeout > 0) { + const gtcResult = await _tryGtcFallback(client, tokenId, tradeSize, price, marketOpts); + if (gtcResult) { + totalSharesFilled = gtcResult.sharesFilled; + totalCostFilled = gtcResult.costFilled; + filled = true; + } + } + if (!filled || totalCostFilled === 0) { logger.error(`Failed to fill buy order for ${market || tokenId} after ${config.maxRetries} attempts`); return;