Best Practices
Guidelines for building robust, safe, and effective trading strategies.
Pick the Right Execution Mode
Every bot has an Execution Mode that controls when and how the strategy sees candles:
continuous(default) — runs every spin interval (e.g., every 60s) against live market data.td.market.*reflects the currently forming candle. Good for scalping, mean-reversion on short timeframes, anything that reacts to intra-bar moves.on_bar_close— runs once per closed bar.td.market.*reflects the last fully closed bar. Backtest parity: signals from your live bot should align with a backtest of the same strategy and candle window within ±1 primary bar. Good for bar-based strategies where you want live behavior to match what backtest shows.
Use on_bar_close if you're tuning your strategy on backtest and want live trades to mirror what the backtest showed. Use continuous if you need to react mid-bar (e.g., breakout confirmation, fast news).
One important detail: td.market.price is always the live ticker, regardless of mode. Use it for order placement. Use td.market.close for bar-based signal logic (it reflects the closed bar under on_bar_close). See Market Data → Execution Mode.
Risk Management
Always Use Stop Losses
Never trade without a stop loss:
// BAD - No risk management
td.trade.buy({ amountPercent: 100 });
// GOOD - Always set stop loss
td.trade.buy({
amountPercent: 100,
stopLoss: td.market.price * 0.98, // 2% stop loss
reason: 'Entry with SL'
});
Position Sizing
Risk a small percentage per trade:
// Risk-based position sizing
const accountBalance = td.account.balance;
const riskPercent = td.config.getNumber('RISK_PERCENT', 1); // Risk 1%
const stopLossPercent = td.config.getNumber('STOP_LOSS_PCT', 2); // 2% SL
// Position size = (Balance × Risk%) / Stop Loss%
const positionValue = (accountBalance * riskPercent) / stopLossPercent;
const positionPercent = (positionValue / accountBalance) * 100;
td.trade.buy({
amountPercent: Math.min(positionPercent, 100),
stopLoss: td.market.price * (1 - stopLossPercent / 100)
});
Maximum Drawdown Protection
Stop trading after significant losses:
const startingBalance = td.state.get('startingBalance', td.account.balance);
const currentBalance = td.account.balance;
const drawdownPercent = ((startingBalance - currentBalance) / startingBalance) * 100;
const maxDrawdown = td.config.getNumber('MAX_DRAWDOWN', 10);
if (drawdownPercent >= maxDrawdown) {
td.utils.log('Max drawdown reached - pausing strategy', {
drawdown: drawdownPercent.toFixed(2) + '%'
});
return;
}
// Update starting balance on new high
if (currentBalance > startingBalance) {
td.state.set('startingBalance', currentBalance);
}
Consecutive Loss Limits
const consecutiveLosses = td.state.get('consecutiveLosses', 0);
const maxConsecutiveLosses = td.config.getNumber('MAX_CONSECUTIVE_LOSSES', 3);
if (consecutiveLosses >= maxConsecutiveLosses) {
td.utils.log('Too many consecutive losses - pausing');
// Reset after cooldown
const pauseStart = td.state.get('pauseStart', Date.now());
const cooldownHours = td.config.getNumber('COOLDOWN_HOURS', 24);
if (Date.now() - pauseStart > cooldownHours * 60 * 60 * 1000) {
td.state.set('consecutiveLosses', 0);
td.state.delete('pauseStart');
}
return;
}
Code Quality
Check Position Before Trading
Always check if you already have a position:
// BAD - Might open multiple positions
if (rsi < 30) {
td.trade.buy();
}
// GOOD - Check position first
if (rsi < 30 && !td.position.hasPosition) {
td.trade.buy();
}
Validate Indicator Values
// Check for valid indicator values
const rsi = td.indicators.rsi;
if (rsi === undefined || isNaN(rsi)) {
td.utils.log('Invalid RSI value');
return;
}
// Ensure sufficient data
if (td.market.candles.length < 50) {
td.utils.log('Insufficient candle data');
return;
}
Handle Edge Cases
// Check for market conditions
const spread = td.market.spread;
const spreadPercent = (spread / td.market.price) * 100;
if (spreadPercent > 0.5) {
td.utils.log('Spread too wide', { spread: spreadPercent });
return;
}
// Check for sufficient balance
if (td.account.availableBalance < 10) {
td.utils.log('Insufficient balance');
return;
}
Use Meaningful Variable Names
// BAD
const x = td.indicators.rsi;
const y = 30;
if (x < y) { /* ... */ }
// GOOD
const currentRsi = td.indicators.rsi;
const oversoldLevel = td.config.getNumber('RSI_OVERSOLD', 30);
if (currentRsi < oversoldLevel) { /* ... */ }
State Management
Clear Stale State
// Periodically clear old state
const lastClear = td.state.get('lastStateClear', 0);
const clearInterval = 24 * 60 * 60 * 1000; // 24 hours
if (Date.now() - lastClear > clearInterval) {
// Keep important state, clear temporary
const importantState = {
totalTrades: td.state.get('totalTrades', 0),
totalWins: td.state.get('totalWins', 0)
openGraph: { title: 'Best Practices', description: 'Best practices for writing reliable custom strategies.' },
};
td.state.clear();
// Restore important state
Object.entries(importantState).forEach(([key, value]) => {
td.state.set(key, value);
});
td.state.set('lastStateClear', Date.now());
}
Don't Store Large Data
// BAD - storing too much
td.state.set('allCandles', td.market.candles);
td.state.set('priceHistory', hugePriceArray);
// GOOD - store only what's needed
td.state.set('lastHigh', Math.max(...td.market.candles.slice(-10).map(c => c.high)));
td.state.set('prevRsi', td.indicators.rsi);
Logging
Log Important Events Only
// BAD - logging every spin
td.utils.log('Spin', { price: td.market.price }); // Creates noise
// GOOD - log meaningful events
if (td.trade.buy({ ... })) {
td.utils.log('BUY SIGNAL', {
price: td.market.price,
rsi: td.indicators.rsi,
reason: 'RSI oversold'
});
}
Use Debug Mode
const DEBUG = td.config.getBoolean('DEBUG_MODE', false);
// Verbose logging only in debug mode
if (DEBUG) {
td.utils.log('Debug info', {
allIndicators: {
rsi: td.indicators.rsi,
macd: td.indicators.macd,
bbands: td.indicators.bbands
}
});
}
Performance
Cache Expensive Calculations
// Helper to get custom indicator value safely
function getIndicator(key) {
const val = td.indicators.custom[key];
if (val === undefined || val === null) return 0;
if (Array.isArray(val)) return val[val.length - 1];
return val;
}
// Cache indicator calculations that don't change within a spin
const ema50 = td.state.get('cachedEma50');
const cacheTime = td.state.get('ema50CacheTime', 0);
// Recalculate every 5 minutes
if (!ema50 || Date.now() - cacheTime > 300000) {
const newEma50 = getIndicator('ema_50');
td.state.set('cachedEma50', newEma50);
td.state.set('ema50CacheTime', Date.now());
}
Avoid Unnecessary Loops
// BAD - inefficient
let sum = 0;
for (let i = 0; i < td.market.candles.length; i++) {
sum += td.market.candles[i].close;
}
const avg = sum / td.market.candles.length;
// GOOD - use built-in methods
const closes = td.market.candles.map(c => c.close);
const avg = closes.reduce((a, b) => a + b, 0) / closes.length;
// BETTER - use built-in indicator
const avg = getIndicator('sma_20');
Trading Logic
Don't Trade Too Frequently
// Implement minimum time between trades
const lastTradeTime = td.state.get('lastTradeTime', 0);
const minTimeBetweenTrades = td.config.getNumber('MIN_TRADE_INTERVAL_SEC', 300) * 1000;
if (Date.now() - lastTradeTime < minTimeBetweenTrades) {
return; // Skip this spin
}
// After trade
td.state.set('lastTradeTime', Date.now());
Confirm Signals
// Require multiple confirmations
const rsiSignal = td.indicators.rsi < 30;
const macdSignal = td.indicators.macd.histogram > 0;
const trendSignal = td.indicators.adx.value > 25 &&
td.indicators.adx.diPlus > td.indicators.adx.diMinus;
// Require at least 2 confirmations
const confirmations = [rsiSignal, macdSignal, trendSignal].filter(Boolean).length;
if (confirmations >= 2 && !td.position.hasPosition) {
td.trade.buy({ reason: `${confirmations} confirmations` });
}
Avoid Over-optimization
// BAD - over-fitted parameters
const RSI_OVERSOLD = 28.735;
const MACD_THRESHOLD = 0.00234;
// GOOD - round, sensible parameters
const RSI_OVERSOLD = td.config.getNumber('RSI_OVERSOLD', 30);
const USE_MACD_CONFIRM = td.config.getBoolean('USE_MACD_CONFIRM', true);
Pre-live Checklist
Before deploying to live trading:
-
Risk Management
- Stop losses on all trades
- Position sizing based on risk
- Maximum drawdown protection
- Consecutive loss limits
-
Code Quality
- Position checks before trading
- Indicator validation
- Edge case handling
- Clear variable names
-
Testing
- Paper trading for minimum period
- Backtest with acceptable results
- All parameters tested
-
Monitoring
- Appropriate logging level
- Debug mode available
- Error handling in place
Next Steps
- Examples - See best practices in action
- Publishing - Share your strategies
- Troubleshooting - Common issues