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:
co-authored by
Claude Sonnet 4.6
parent
365dc4e164
commit
0148b0bbef
@@ -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)
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user