Core Agent Logic
Now we bring together all components to build the main trading loop. This is where the price oracle, o2 client, and math utilities work together to execute trades.
Main Agent Controller
This is the heart of your trading agent - it orchestrates price fetching, order calculation, and automated trading cycles.
Create the main agent file:
touch src/index.tsAdd the following content to src/index.ts:
import { loadConfig } from './config';
import { BitgetClient } from './bitget';
import { O2Client } from './o2';
import { createLogger } from './utils/logger';
import { BotConfig, MarketConfig } from './types/config';
import Decimal from 'decimal.js';
import { OrderSide } from '../lib/index';
import { scaleUpAndTruncateToInt, calculateBaseQuantity } from './utils/numbers';
import { shortenAxiosError } from '../lib/rest-api/utils/httpRequest';
// Constants
// Symbol for USDC/USDT pair on Bitget - used when converting prices to USDC
const USDC_USDT_SYMBOL = 'USDC/USDT';
// Load configuration from YAML file
const configPath = process.env.CONFIG_PATH || 'config.yaml';
const config: BotConfig = loadConfig(configPath);
// Main logic - wrapped in async IIFE for top-level await
(async () => {
// Create logger with configured log level (defaults to 'info')
const logger = createLogger(config.general?.log_level?.toLowerCase() || 'info');
// Initialize Bitget client for fetching external price data
const bitgetClient = new BitgetClient();
// Initialize O2 client for placing orders on the exchange
const o2Client = new O2Client(config.o2.base_url, config.general.network_url, logger);
// Initialize the O2 client with session key and market contract
await o2Client.init(config.o2.account.private_key, config.o2.market.contract_id);
// Worker function - executes a single trading cycle (buy + sell)
async function marketWorker(marketConfig: MarketConfig, isRunningRef: { value: boolean }) {
// Prevent overlapping cycles - skip if previous cycle still running
if (isRunningRef.value) {
logger.warn(
`Previous job still running for market ${marketConfig.base_symbol}/${marketConfig.quote_symbol}, skipping this cycle.`
);
return;
}
isRunningRef.value = true;
logger.debug(`Starting worker for market ${marketConfig.base_symbol}/${marketConfig.quote_symbol}`);
try {
// Fetch market metadata from O2 (decimals, precision, etc.)
const market = await o2Client.getMarket(marketConfig);
if (!market) {
logger.error(`Market not found on O2: ${marketConfig.base_symbol}/${marketConfig.quote_symbol}`);
throw new Error('Market not found');
}
// Fetch reference price from Bitget
let price: Decimal;
try {
// Get base price from Bitget (e.g., ETH/USDC)
price = new Decimal(await bitgetClient.fetchPrice(marketConfig.bitget_symbol));
// Convert to USDC if needed (e.g., FUEL/USDT -> FUEL/USDC)
if (marketConfig.convert_to_usdc) {
logger.debug('Converting price to USDC using USDC/USDT pair');
const usdc_usdt_price = await bitgetClient.fetchPrice(USDC_USDT_SYMBOL);
// Convert: (FUEL/USDT) / (USDC/USDT) = FUEL/USDC
price = price.div(usdc_usdt_price);
}
// Apply reciprocal rate if needed (e.g., USDC/USDT -> USDT/USDC)
if (marketConfig.reciprocal_rate) {
logger.debug(`Applying reciprocal rate: 1/${price} = ${new Decimal(1).div(price)}`);
price = new Decimal(1).div(price);
}
logger.info(`Fetched Bitget price for ${marketConfig.base_symbol}/${marketConfig.quote_symbol}: ${price}`);
} catch (err) {
logger.error(
{ err },
`Failed to fetch Bitget price for ${marketConfig.base_symbol}/${marketConfig.quote_symbol}. Skipping this cycle.`
);
return;
}
// Calculate adjusted buy price (increase by adjustment factor to ensure fill)
const buyPrice = scaleUpAndTruncateToInt(
price.mul(1 + marketConfig.price_adjustment_factor),
market.quote.decimals,
market.quote.max_precision
).toString();
// Calculate adjusted sell price (decrease by adjustment factor to ensure fill)
const sellPrice = scaleUpAndTruncateToInt(
price.mul(1 - marketConfig.price_adjustment_factor),
market.quote.decimals,
market.quote.max_precision
).toString();
logger.info(
`Final buy/sell prices: ${new Decimal(buyPrice).toString()}/${new Decimal(sellPrice).toString()} with adjustment ${marketConfig.price_adjustment_factor}`
);
// Calculate order quantity based on USDC value and current price
const usdcValue = new Decimal(marketConfig.order_usdc_value);
const quantityDecimal = calculateBaseQuantity(usdcValue, price, market.base.decimals, market.base.max_precision);
const quantity = quantityDecimal.toString();
// Place buy order on O2
let buyOrderSuccess = false;
try {
const start = Date.now();
buyOrderSuccess = await o2Client.placeOrder(market, buyPrice, quantity, OrderSide.Buy, marketConfig.order_type);
logger.info(
`Buy order placed for ${marketConfig.base_symbol}/${marketConfig.quote_symbol}; price ${buyPrice}, quantity ${quantity} with latency ${Date.now() - start} ms`
);
} catch (err) {
logger.error(
{ err: shortenAxiosError(err) },
`Buy order failed for ${marketConfig.base_symbol}/${marketConfig.quote_symbol}. Trying the sell anyways.`
);
}
if (!buyOrderSuccess) {
logger.error(
`Buy order unsuccessful for ${marketConfig.base_symbol}/${marketConfig.quote_symbol}. Trying the sell anyways.`
);
}
// Wait between buy and sell orders (order_interval_seconds)
await sleep(marketConfig.order_interval_seconds * 1000);
// Place sell order on O2 (continues even if buy failed)
let sellOrderSuccess = false;
try {
const start = Date.now();
sellOrderSuccess = await o2Client.placeOrder(
market,
sellPrice,
quantity,
OrderSide.Sell,
marketConfig.order_type
);
logger.info(
`Sell order placed for ${marketConfig.base_symbol}/${marketConfig.quote_symbol}; price ${sellPrice}, quantity ${quantity} with latency ${Date.now() - start} ms`
);
} catch (err) {
logger.error(
{ err: shortenAxiosError(err) },
`Sell order failed for ${marketConfig.base_symbol}/${marketConfig.quote_symbol}`
);
}
if (!sellOrderSuccess) {
logger.error(`Sell order unsuccessful for ${marketConfig.base_symbol}/${marketConfig.quote_symbol}`);
}
} catch (err) {
logger.error(
{ err },
`Unexpected error in market worker for ${marketConfig.base_symbol}/${marketConfig.quote_symbol}`
);
throw err;
} finally {
// Release the lock so next cycle can run
isRunningRef.value = false;
}
}
// Sleep utility - returns a promise that resolves after ms milliseconds
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Custom scheduler that supports fractional seconds
// Runs trading cycles at fixed intervals (non-blocking)
async function startScheduler(marketConfig: MarketConfig, intervalSeconds: number, isRunningRef: { value: boolean }) {
logger.info(
`Starting scheduler for O2 market ${marketConfig.base_symbol}/${marketConfig.quote_symbol} with interval ${intervalSeconds} seconds.`
);
const sleepMs = intervalSeconds * 1000;
// Infinite loop - runs trading cycles continuously
while (true) {
// Start the job without awaiting (non-blocking - allows overlap prevention)
marketWorker(marketConfig, isRunningRef).catch((err) => {
logger.error({ err }, 'Error in scheduled job execution');
});
// Wait for interval before next cycle (timer starts immediately)
await sleep(sleepMs);
}
}
// Initialize scheduler with market configuration
const marketConfig = config.o2.market;
const intervalSeconds = Number(marketConfig.order_pairs_interval_seconds);
const isRunningRef = { value: false }; // Shared state for overlap prevention
// Start the scheduler (runs indefinitely)
startScheduler(marketConfig, intervalSeconds, isRunningRef);
})();Understanding the Trading Loop
The agent operates in cycles:
Cycle 1 (t=0s):
├─ Fetch price: $2500
├─ Calculate: Buy $2750, Sell $2250
├─ Place buy order
├─ Wait 1 second
├─ Place sell order
└─ Wait 2.5 seconds
Cycle 2 (t=2.5s):
├─ Fetch price: $2505
├─ Calculate: Buy $2755.50, Sell $2254.50
├─ Place buy order
├─ Wait 1 second
├─ Place sell order
└─ Wait 2.5 seconds
Cycle 3 (t=5s):
...Timing Parameters
order_interval_seconds
Time between buy and sell orders within a cycle:
order_interval_seconds: 1 # 1 second between buy and sellTypical values: 0.5 - 2 seconds
order_pairs_interval_seconds
Time between complete cycles:
order_pairs_interval_seconds: 2.5 # 2.5 seconds between cyclesCalculation:
Cycles per hour = 3600 / order_pairs_interval_seconds
Volume per hour = cycles_per_hour × order_usdc_value × 2
Example:
3600 / 2.5 = 1440 cycles/hour
1440 × $10 × 2 = $28,800/hourTypical values: 2 - 5 seconds
Overlap Prevention
The isRunningRef flag prevents overlapping cycles:
if (isRunningRef.value) {
logger.warn('Previous cycle still running, skipping');
return;
}
isRunningRef.value = true;If a cycle takes longer than order_pairs_interval_seconds, it will skip the next scheduled execution rather than queuing up.
Error Handling Strategy
The bot uses a continue-on-error approach:
try {
buyOrderSuccess = await o2Client.placeOrder(...);
} catch (err) {
logger.error({ err }, 'Buy order failed, continuing to sell');
}
// Still attempts sell order even if buy failsWhy? A failed buy order shouldn't prevent the sell order. The agent prioritizes uptime over perfect execution.