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 <noreply@anthropic.com>
This commit is contained in:
direkturcrypto
2026-02-25 04:30:12 +07:00
co-authored by Claude Sonnet 4.6
parent 365dc4e164
commit 0148b0bbef
3 changed files with 80 additions and 0 deletions
+5
View File
@@ -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)
# ─────────────────────────────────────────────
+4
View File
@@ -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),
+71
View File
@@ -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;