
Recently, American AI lab nof1ai launched a compelling experiment—giving six top AI models (Claude, DeepSeek, Gemini, GPT-5, Grok, and Tongyi Qianwen) $10,000 each to compete in the real cryptocurrency perpetual futures market. This wasn’t a simulation, but actual trading with real money. The results were surprising: China’s DeepSeek consistently led the pack, while both GPT-5 and Gemini crashed and burned. The greatest value of this competition lies in the team’s decision to open-source all the prompts, data structures, and decision-making processes they used, providing us with an excellent blueprint for studying AI quantitative trading.

This article will provide a detailed breakdown of every node and logic element in an AI quantitative trading workflow based on this publicly available architecture, helping you understand how AI makes trading decisions in real markets.
This AI quantitative trading system employs the classic “Perception-Decision-Execution” three-stage architecture, with the entire workflow composed of multiple core nodes:

Core Node Descriptions:
Let’s dive deep into the design logic of each node one by one.
The timer trigger is the initiator of the entire workflow, similar to the human heartbeat, periodically driving the system’s operation.
This is a critical state management node responsible for initializing, tracking, and calculating various core account metrics.
// Set Binance testnet API base URL
api_base = "https://testnet.binancefuture.com"
exchange.SetBase(api_base)
// Initialization check - set baseline values on first run
if (_G('invoketime') === null) {
_G('invoketime', 0);
_G('STARTTIME', Date.now());
const initAccount = exchange.GetAccount();
_G('initmoney', initAccount.Equity);
}
// Accumulate invocation count
const invoketime = _G('invoketime') + 1;
_G('invoketime', invoketime);
// Calculate runtime duration (minutes)
const duringtime = Math.floor((Date.now() - _G('STARTTIME')) / 60000);
// Get current account information
const currentAccount = exchange.GetAccount();
const currentAccountValue = currentAccount.Equity;
// Calculate total return percentage
const initMoney = _G('initmoney');
const totalReturnPercent = ((currentAccountValue - initMoney) / initMoney * 100).toFixed(2);
// Log profit to system
LogProfit(currentAccountValue - initMoney, "&")
// Return 5 core metrics
return [{
json: {
invoketime: invoketime,
duringtime: duringtime,
totalReturnPercent: totalReturnPercent + '%',
availableCash: currentAccount.Balance.toFixed(2),
currentAccountValue: currentAccountValue.toFixed(2)
}
}];
{
"invoketime": 42, // System invocation count
"duringtime": 126, // Runtime duration (minutes)
"totalReturnPercent": "3.45%", // Total return percentage
"availableCash": "10345.67", // Available cash
"currentAccountValue": "10345.00" // Total account value
}
This is the “eyes” of the entire system, responsible for collecting, processing, and calculating various market data and technical indicators.
The market data acquisition node’s functionality can be divided into three levels:
// Parse coin list
const coins = $vars.coinList ?
($vars.coinList.includes(',') ? $vars.coinList.split(',') : [$vars.coinList]) : [];
if (coins.length === 0) {
return {};
}
// Fetch market data for each coin
const allCoinsData = {};
for (let i = 0; i < coins.length; i++) {
const coin = coins[i].trim();
try {
allCoinsData[coin] = getMarketDataForCoin(coin);
} catch (e) {
allCoinsData[coin] = { error: e.toString() };
}
}
return { data: allCoinsData };
function getMarketDataForCoin(symbol) {
// Switch to the corresponding coin's perpetual contract
exchange.SetCurrency(symbol + "_USDT");
exchange.SetContractType("swap");
// Fetch 3-minute and 4-hour candlestick data
const kline3m = exchange.GetRecords(60 * 3); // 3-minute timeframe
const kline4h = exchange.GetRecords(60 * 60 * 4); // 4-hour timeframe
// Data validity check
if (!kline3m || kline3m.length < 50 || !kline4h || kline4h.length < 50) {
return { error: "Insufficient candlestick data" };
}
// Calculate 3-minute timeframe technical indicators
const ema20_3m = TA.EMA(kline3m, 20); // 20-period Exponential Moving Average
const macd_3m = TA.MACD(kline3m, 12, 26, 9); // MACD indicator
const rsi7_3m = TA.RSI(kline3m, 7); // 7-period RSI
const rsi14_3m = TA.RSI(kline3m, 14); // 14-period RSI
// Calculate 4-hour timeframe technical indicators
const ema20_4h = TA.EMA(kline4h, 20); // 20-period EMA
const ema50_4h = TA.EMA(kline4h, 50); // 50-period EMA
const macd_4h = TA.MACD(kline4h, 12, 26, 9); // MACD indicator
const rsi14_4h = TA.RSI(kline4h, 14); // 14-period RSI
const atr3_4h = TA.ATR(kline4h, 3); // 3-period ATR (volatility)
const atr14_4h = TA.ATR(kline4h, 14); // 14-period ATR
// Get latest candlestick and recent 10 candlesticks
const latest3m = kline3m[kline3m.length - 1];
const latest4h = kline4h[kline4h.length - 1];
const recent10_3m = kline3m.slice(-10);
const recent10_4h = kline4h.slice(-10);
// Calculate average volume
const volumes4h = recent10_4h.map(k => k.Volume);
const avgVolume4h = volumes4h.reduce((a, b) => a + b, 0) / volumes4h.length;
// Return structured data...
}
Dual Timeframe Strategy:
This multi-timeframe analysis is a standard configuration in professional quantitative trading, essentially providing the AI with both a “microscope” and a “telescope” perspective simultaneously.
return {
symbol: symbol,
current_price: latest3m.Close,
current_ema20: ema20_3m[ema20_3m.length - 1],
current_macd: macd_3m[2][macd_3m[2].length - 1],
current_rsi_7: rsi7_3m[rsi7_3m.length - 1],
funding_rate: fundingRate,
intraday_3min: {
mid_prices: recent10_3m.map(k => k.Close),
ema_20_series: recent10_3m.map((k, i) => ema20_3m[ema20_3m.length - 10 + i]),
macd_series: recent10_3m.map((k, i) => macd_3m[2][macd_3m[2].length - 10 + i]),
rsi_7_series: recent10_3m.map((k, i) => rsi7_3m[rsi7_3m.length - 10 + i]),
rsi_14_series: recent10_3m.map((k, i) => rsi14_3m[rsi14_3m.length - 10 + i])
},
longer_term_4hour: {
ema_20: ema20_4h[ema20_4h.length - 1],
ema_50: ema50_4h[ema50_4h.length - 1],
atr_3: atr3_4h[atr3_4h.length - 1],
atr_14: atr14_4h[atr14_4h.length - 1],
current_volume: latest4h.Volume,
average_volume: avgVolume4h,
macd_series: recent10_4h.map((k, i) => macd_4h[2][macd_4h[2].length - 10 + i]),
rsi_14_series: recent10_4h.map((k, i) => rsi14_4h[rsi14_4h.length - 10 + i])
}
};
The position data acquisition node is responsible for tracking the current position status of each coin, providing the AI with complete position information.
function getAllPositions() {
// Get current account equity
const curequity = exchange.GetAccount().Equity;
// Get coin list
const coins = $vars.coinList ?
($vars.coinList.includes(',') ? $vars.coinList.split(',') : [$vars.coinList]) : [];
// Calculate risk amount per coin = account equity / number of coins
const risk_usd = coins.length > 0 ? curequity / coins.length : 0;
// Get all actual positions
const rawPositions = exchange.GetPositions();
// Create position mapping table (coin symbol -> position object)
const positionMap = {};
if (rawPositions && rawPositions.length > 0) {
for (let pos of rawPositions) {
if (pos.Amount && Math.abs(pos.Amount) > 0) {
// Extract coin symbol (e.g., BTC_USDT.swap -> BTC)
const coinSymbol = pos.Symbol.replace('_USDT.swap', '')
.replace('.swap', '')
.replace('_USDT', '');
positionMap[coinSymbol] = pos;
}
}
}
// Create position information for each coin
const allPositions = [];
for (let i = 0; i < coins.length; i++) {
const coin = coins[i].trim();
const pos = positionMap[coin];
if (pos) {
// When position exists - build complete position information
// Get take-profit and stop-loss order IDs
const { tpOrderId, slOrderId } = getTPSLOrderIds(pos.Symbol, currentPrice, pos.Type);
// Get exit plan
const exitPlan = _G(`exit_plan_${pos.Symbol}`) || {
profit_target: null,
stop_loss: null,
invalidation_condition: ""
};
allPositions.push({
symbol: coin,
quantity: Math.abs(pos.Amount), // Position size
entry_price: pos.Price, // Entry price
current_price: currentPrice, // Current price
liquidation_price: /* Liquidation price calculation */,
unrealized_pnl: _N(pos.Profit, 2), // Unrealized P&L
leverage: pos.MarginLevel || 1, // Leverage multiplier
exit_plan: exitPlan, // Exit plan
confidence: exitPlan?.confidence || null,
risk_usd: risk_usd, // Risk amount
sl_oid: slOrderId, // Stop-loss order ID
tp_oid: tpOrderId, // Take-profit order ID
wait_for_fill: false,
entry_oid: pos.Info?.posId || -1,
notional_usd: _N(Math.abs(pos.Amount) * currentPrice, 2)
});
} else {
// When no position exists - set all fields to null
allPositions.push({
symbol: coin,
quantity: null, // Key field: null indicates no position
entry_price: null,
current_price: null,
liquidation_price: null,
unrealized_pnl: null,
leverage: null,
exit_plan: null,
confidence: null,
risk_usd: risk_usd, // Still return risk_usd for opening position calculations
sl_oid: null,
tp_oid: null,
wait_for_fill: false,
entry_oid: null,
notional_usd: null
});
}
}
return allPositions;
}
const positions = getAllPositions();
return {positions};
Critical role of the quantity field:
quantity = null: No position, AI can consider opening a positionquantity ≠ null: Position exists, AI can only hold or close, cannot add to positionThis design avoids the risk of “pyramid position sizing” and ensures each coin has only one active position.
Merges market data and position data into the complete context required for AI decision-making.
// Get input data
const inputData = $input.all();
// First input is market data, second is position data
const marketData = inputData[0].json.data;
const positions = inputData[1].json;
// Return organized data
return [{
json: {
marketData: marketData,
positions: positions
}
}];
Simple and efficient data integration, providing a unified data interface for the AI.
This is the core of the entire system—the AI decision engine. It receives complete market and account information and outputs specific trading instructions.

The AI agent’s decision-making relies on two types of prompts:
This dual-prompt architecture of “fixed rules + dynamic data” allows the AI to maintain a stable decision-making framework while responding flexibly to real-time market conditions.
The system prompt defines the AI’s behavioral rules and decision-making framework, which can be divided into four core parts:
## HARD CONSTRAINTS
### Position Limits
- Tradable coins: {{$vars.coinList}}
- Maximum {{$vars.coinList.split(',').length}} concurrent positions
- No pyramiding or adding to existing positions
- Must be flat before re-entering a coin
### Risk Management
- Maximum risk per trade: 5% of account value
- Leverage range: 5x to 40x
- Minimum risk-reward ratio: 2:1
- Every position must have:
- Stop loss (specific price level)
- Profit target (specific price level)
- Invalidation condition (format: "If price closes below/above [PRICE] on a [TIMEFRAME] candle")
## DECISION FRAMEWORK
### Identifying Position Status
A coin has an **active position** if `quantity` is NOT null.
A coin has **no position** if `quantity` is null.
### For Coins WITH Active Positions
Check each position in this order:
1. Invalidation condition triggered? → Close immediately
2. Stop loss or profit target hit? → Close
3. Technical setup still valid? → Hold
4. If uncertain → Hold (trust your exit plan)
Available signals: "hold" or "close" ONLY
### For Coins WITHOUT Positions
Only consider if:
- Current active positions < 6
- Available cash > $1000
- You see a high-probability setup
Available signal: "entry" ONLY
{
"BTC": {
"trade_signal_args": {
"coin": "BTC",
"signal": "entry|hold|close",
"profit_target": 115000.0,
"stop_loss": 112000.0,
"invalidation_condition": "If price closes below 112500 on a 3-minute candle",
"leverage": 15,
"confidence": 0.75,
"risk_usd": 624.38,
"justification": "Reason for this decision"
}
}
}
## THINKING PROCESS
Before outputting JSON, think through your decisions step by step:
1. Identify position status for each coin
2. Review coins WITH active positions
3. Review coins WITHOUT positions
4. Output final decisions in JSON
This “Chain of Thought” prompting makes the AI analyze before outputting, improving decision stability and interpretability.
Source Explanation: The user prompt uses template syntax to dynamically inject real-time data.
The user prompt is dynamically generated with the latest market conditions and account information on each call:
It has been {{ duringtime }} minutes since you started trading.
The current time is {{ $now.toISO() }}
You've been invoked {{ invoketime }} times.
ALL OF THE PRICE OR SIGNAL DATA BELOW IS ORDERED: OLDEST - NEWEST
Timeframes note: Unless stated otherwise in a section title, intraday series
are provided at 3 minute intervals. If a coin uses a different interval, it is
explicitly stated in that coin's section.
**CURRENT MARKET STATE FOR ALL COINS**
{{ JSON.stringify(marketData) }}
**HERE IS YOUR ACCOUNT INFORMATION & PERFORMANCE**
Current Total Return (percent): {{ totalReturnPercent }}
Available Cash: {{ availableCash }}
Current Account Value: {{ currentAccountValue }}
Current live positions & performance:
{{ JSON.stringify(positions) }}
This design ensures the AI receives complete decision-making context each time:
┌─────────────────────────────────────────────────────────────┐
│ AI Model Invocation Moment │
├─────────────────────────────────────────────────────────────┤
│ │
│ System Prompt (Fixed) │
│ ┌────────────────────────────────────────────┐ │
│ │ You are an expert cryptocurrency trader... │ │
│ │ ## HARD CONSTRAINTS │ │
│ │ - Maximum 6 positions │ │
│ │ - Risk per trade: 5% max │ │
│ │ ## DECISION FRAMEWORK │ │
│ │ - For coins WITH positions: hold/close │ │
│ │ - For coins WITHOUT positions: entry │ │
│ │ ## OUTPUT FORMAT │ │
│ │ - JSON with trade_signal_args │ │
│ └────────────────────────────────────────────┘ │
│ ↓ │
│ User Prompt (Dynamic, different each time) │
│ ┌────────────────────────────────────────────┐ │
│ │ Running for 126 minutes, invoked 42 times │ │
│ │ Current Total Return: 3.45% │ │
│ │ Available Cash: $10,345.67 │ │
│ │ │ │
│ │ Market Data: │ │
│ │ { │ │
│ │ "BTC": { │ │
│ │ "current_price": 67234.5, │ │
│ │ "current_ema20": 67150.2, │ │
│ │ "current_macd": -141.87, │ │
│ │ "current_rsi_7": 52.93, │ │
│ │ "intraday_3min": {...}, │ │
│ │ "longer_term_4hour": {...} │ │
│ │ }, │ │
│ │ "ETH": {...}, ... │ │
│ │ } │ │
│ │ │ │
│ │ Positions: │ │
│ │ [ │ │
│ │ { │ │
│ │ "symbol": "BTC", │ │
│ │ "quantity": 0.5, │ │
│ │ "entry_price": 66800.0, │ │
│ │ "unrealized_pnl": 217.25, │ │
│ │ "exit_plan": { │ │
│ │ "stop_loss": 66000.0, │ │
│ │ "profit_target": 68500.0 │ │
│ │ } │ │
│ │ }, │ │
│ │ { │ │
│ │ "symbol": "ETH", │ │
│ │ "quantity": null, // No position │ │
│ │ ... │ │
│ │ } │ │
│ │ ] │ │
│ └────────────────────────────────────────────┘ │
│ ↓ │
│ AI Model Processing and Reasoning │
│ ↓ │
│ AI Output (JSON Format) │
│ ┌────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "BTC": { │ │
│ │ "trade_signal_args": { │ │
│ │ "signal": "hold", │ │
│ │ "justification": "Position valid..." │ │
│ │ } │ │
│ │ }, │ │
│ │ "ETH": { │ │
│ │ "trade_signal_args": { │ │
│ │ "signal": "entry", │ │
│ │ "profit_target": 2800.0, │ │
│ │ "stop_loss": 2600.0, │ │
│ │ "leverage": 15, │ │
│ │ "risk_usd": 833.33, │ │
│ │ "justification": "Bullish setup..." │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ └────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
↓
Passed to "Trade Execution" Node
Advantages of This Dual-Prompt Architecture:
This is the “hands” of the workflow, responsible for translating AI decisions into actual exchange orders and continuously monitoring take-profit and stop-loss conditions.
The trade execution node is divided into five functional modules:
// ========== Utility Functions ==========
function parseAIOutput(output) {
try {
// Clean code block markers from output
const cleaned = output.replace(/```[a-z]*\n?/gi, '').trim();
const start = cleaned.indexOf('{');
const end = cleaned.lastIndexOf('}');
return JSON.parse(cleaned.substring(start, end + 1));
} catch (e) {
return {};
}
}
// Main logic
const signals = parseAIOutput($input.first().json.output);
Log(signals)
// Get trading pair precision information
function getPrecision(coin) {
try {
const symbol = coin + '_USDT.swap';
const markets = exchange.GetMarkets();
if (markets && markets[symbol]) {
return {
price: markets[symbol].PricePrecision || 0,
amount: markets[symbol].AmountPrecision || 0,
minQty: markets[symbol].MinQty || 5
};
}
return { price: 0, amount: 0, minQty: 5 };
} catch (e) {
Log(`⚠️ Failed to get ${coin} precision, using default values`);
return { price: 0, amount: 0, minQty: 5 };
}
}
// Calculate position size
function calculateQuantity(entryPrice, stopLoss, riskUsd, leverage, precision) {
const riskPerContract = Math.abs(entryPrice - stopLoss);
if (riskPerContract <= 0) return 0;
// Calculate quantity based on risk amount
const quantity = riskUsd / riskPerContract;
// Limit maximum position = (account balance × leverage / 6) / entry price
const maxQuantity = (exchange.GetAccount().Balance * leverage / 6) / entryPrice;
let finalQuantity = Math.min(quantity, maxQuantity);
// Apply precision and minimum quantity limits
if (precision) {
finalQuantity = _N(finalQuantity, precision.amount);
if (finalQuantity < precision.minQty) {
Log(`⚠️ Calculated quantity ${finalQuantity} is less than minimum order quantity ${precision.minQty}`);
return 0;
}
}
return finalQuantity;
}
Risk Distance = |Entry Price - Stop Loss Price|
Position Size = Risk Amount ÷ Risk Distance
Maximum Position = (Account Balance × Leverage ÷ 6) ÷ Entry Price
Final Position = min(Position Size, Maximum Position)
This calculation method ensures:
function executeEntry(coin, args) {
exchange.SetCurrency(coin + '_USDT');
exchange.SetContractType("swap");
const ticker = exchange.GetTicker();
if (!ticker) return;
const currentPrice = ticker.Last;
if (!validateEntry(coin, currentPrice, args.profit_target, args.stop_loss)) return;
const leverage = args.leverage || 10;
exchange.SetMarginLevel(leverage);
precision = getPrecision(coin);
quantity = calculateQuantity(currentPrice, args.stop_loss, args.risk_usd, leverage, precision);
if (quantity <= 0) {
Log(`⚠️ ${coin}: Calculated quantity invalid, skipping entry`);
return;
}
const isLong = args.profit_target > args.stop_loss;
exchange.SetDirection(isLong ? "buy" : "sell");
const orderId = isLong ? exchange.Buy(-1, quantity) : exchange.Sell(-1, quantity);
if (orderId) {
Sleep(1000);
Log(`✅ ${coin}: Opened ${isLong ? 'Long' : 'Short'} Quantity=${quantity} Leverage=${leverage}x Precision=${precision.amount}`);
} else {
Log(`❌ ${coin}: Failed to open position`, precision.amount);
}
}
function executeClose(coin) {
exchange.SetCurrency(coin + '_USDT');
exchange.SetContractType("swap");
// Cancel all pending orders
const orders = exchange.GetOrders();
orders?.forEach(o => exchange.CancelOrder(o.Id));
// Get position information
const pos = exchange.GetPositions().find(p =>
p.Symbol.includes(coin) && Math.abs(p.Amount) > 0
);
if (!pos) return;
const isLong = pos.Type === PD_LONG || pos.Type === 0;
const precision = getPrecision(coin);
// Apply precision to closing quantity
const closeAmount = _N(Math.abs(pos.Amount), precision.amount);
exchange.SetDirection(isLong ? "closebuy" : "closesell");
const orderId = isLong ?
exchange.Sell(-1, closeAmount) :
exchange.Buy(-1, closeAmount);
if (orderId) {
Log(`✅ ${coin}: Successfully closed ${isLong ? 'Long' : 'Short'}, Quantity=${closeAmount}`);
_G(`exit_plan_${coin}_USDT.swap`, null);
}
}
function monitorPosition(coin) {
exchange.SetCurrency(coin + '_USDT');
exchange.SetContractType("swap");
const pos = exchange.GetPositions().find(p =>
p.Symbol.includes(coin) && Math.abs(p.Amount) > 0
);
if (!pos) return;
const ticker = exchange.GetTicker();
if (!ticker) return;
const isLong = pos.Type === PD_LONG || pos.Type === 0;
const currentPrice = ticker.Last;
// Calculate profit/loss percentage
const pnl = (currentPrice - pos.Price) * (isLong ? 1 : -1) / pos.Price;
// Get exit plan
const exitPlan = _G(`exit_plan_${coin}_USDT.swap`);
if (!exitPlan?.profit_target || !exitPlan?.stop_loss) {
// If no exit plan is set, use default values
if (pnl >= 0.03) return closePosition(coin, pos, isLong, "Take Profit", pnl);
if (pnl <= -0.01) return closePosition(coin, pos, isLong, "Stop Loss", pnl);
return;
}
// Check take-profit condition
const shouldTP = isLong ?
currentPrice >= exitPlan.profit_target :
currentPrice <= exitPlan.profit_target;
// Check stop-loss condition
const shouldSL = isLong ?
currentPrice <= exitPlan.stop_loss :
currentPrice >= exitPlan.stop_loss;
if (shouldTP) return closePosition(coin, pos, isLong, "Take Profit", pnl);
if (shouldSL) return closePosition(coin, pos, isLong, "Stop Loss", pnl);
}
function closePosition(coin, pos, isLong, reason, pnl) {
const precision = getPrecision(coin);
const closeAmount = _N(Math.abs(pos.Amount), precision.amount);
exchange.SetDirection(isLong ? "closebuy" : "closesell");
isLong ? exchange.Sell(-1, closeAmount) : exchange.Buy(-1, closeAmount);
Log(`${reason === 'Take Profit' ? '✅' : '❌'} ${coin} ${reason} ${(pnl*100).toFixed(2)}%`);
_G(`exit_plan_${coin}_USDT.swap`, null);
}
To facilitate understanding of the AI trading operation logic, the AI trading signal analysis and position status are presented visually.
图片
The system simultaneously uses 3-minute and 4-hour timeframes, which is not simple data redundancy:
First Line: Capital Allocation
Maximum risk per trade = Account value ÷ Number of coins
Maximum 6 positions = Theoretical maximum total risk 30%
Actual maximum risk < 30% (not all 6 stop-losses will trigger simultaneously)
Second Line: Stop-Loss Setup
Stop-loss distance = |Entry price - Stop-loss price| / Entry price
Typical setting: 2-5%
Combined with leverage: At 10x leverage, 5% price movement = 50% principal movement
Third Line: Invalidation Conditions
javascriptExample: "If price closes below $66,500 on the 3-minute candlestick"
Purpose: Exit even if stop-loss hasn't triggered when technical pattern breaks
Why require JSON format?
Why enforce field consistency?
This AI quantitative trading system demonstrates how to apply large language models to real financial trading scenarios. Its core advantages include:
Key Insights:
I hope this detailed technical breakdown helps you understand the operational logic of AI quantitative trading and make wiser decisions in your own practice.
Complete Source Code and Live Trading Records:
Risk Warning: