State Management

Learn how to persist data across strategy spins using the td.state API.

Overview

Strategies execute periodically (spins) and don't naturally retain data between executions. The td.state API provides persistent storage that survives across spins, allowing your strategy to:

  • Track historical values
  • Count events
  • Implement complex logic spanning multiple spins
  • Store calculated values for efficiency

Basic Usage

Setting and Getting Values

// Store a value
td.state.set('myKey', 'myValue');

// Retrieve a value with default
const value = td.state.get('myKey', 'defaultValue');

// Retrieve without default (returns undefined if not found)
const maybeValue = td.state.get('myKey');

Supported Data Types

// Strings
td.state.set('status', 'active');

// Numbers
td.state.set('tradeCount', 42);

// Booleans
td.state.set('inPosition', true);

// Objects
td.state.set('lastTrade', {
    price: 50000,
    time: Date.now(),
    side: 'buy'
});

// Arrays
td.state.set('priceHistory', [50000, 50100, 49900]);

Common Patterns

Tracking Previous Values

// Detect RSI crossovers
const currentRsi = td.indicators.rsi;
const previousRsi = td.state.get('previousRsi', currentRsi);

// Crossed above 30 (oversold exit)
if (previousRsi < 30 && currentRsi >= 30) {
    td.utils.log('RSI crossed above 30');
}

// Crossed below 70 (overbought exit)
if (previousRsi > 70 && currentRsi <= 70) {
    td.utils.log('RSI crossed below 70');
}

// Save for next spin
td.state.set('previousRsi', currentRsi);

Counting Events

// Count consecutive losses
if (td.position.hasPosition && td.position.pnl < 0) {
    const losses = td.state.get('consecutiveLosses', 0);
    td.state.set('consecutiveLosses', losses + 1);
} else if (td.position.pnl > 0) {
    td.state.set('consecutiveLosses', 0);
}

// Pause after too many losses
if (td.state.get('consecutiveLosses', 0) >= 3) {
    td.utils.log('Pausing after 3 consecutive losses');
    return;
}

Implementing Cooldowns

// Cooldown between trades
const lastTradeTime = td.state.get('lastTradeTime', 0);
const cooldownMs = 60 * 60 * 1000; // 1 hour

if (Date.now() - lastTradeTime < cooldownMs) {
    td.utils.log('In cooldown period');
    return;
}

// After executing trade
if (tradeExecuted) {
    td.state.set('lastTradeTime', Date.now());
}

Building Price History

// Maintain rolling price history
const maxHistory = 100;
let priceHistory = td.state.get('priceHistory', []);

// Add current price
priceHistory.push(td.market.price);

// Keep only last N prices
if (priceHistory.length > maxHistory) {
    priceHistory = priceHistory.slice(-maxHistory);
}

td.state.set('priceHistory', priceHistory);

// Calculate custom moving average
const avg = priceHistory.reduce((a, b) => a + b, 0) / priceHistory.length;

Tracking Trade Statistics

// Update win/loss statistics on position close
if (!td.position.hasPosition) {
    const lastPnl = td.state.get('lastPositionPnl');

    if (lastPnl !== undefined) {
        const wins = td.state.get('totalWins', 0);
        const losses = td.state.get('totalLosses', 0);
        const totalPnl = td.state.get('totalPnl', 0);

        if (lastPnl > 0) {
            td.state.set('totalWins', wins + 1);
        } else {
            td.state.set('totalLosses', losses + 1);
        }
        td.state.set('totalPnl', totalPnl + lastPnl);
        td.state.delete('lastPositionPnl');
    }
}

// Track current position PnL
if (td.position.hasPosition) {
    td.state.set('lastPositionPnl', td.position.pnl);
}

// Calculate win rate
const wins = td.state.get('totalWins', 0);
const losses = td.state.get('totalLosses', 0);
const winRate = wins + losses > 0 ? wins / (wins + losses) : 0;

State Machine Pattern

// Strategy states
const STATE_WAITING = 'waiting';
const STATE_ENTERING = 'entering';
const STATE_IN_POSITION = 'in_position';
const STATE_EXITING = 'exiting';

const currentState = td.state.get('strategyState', STATE_WAITING);

switch (currentState) {
    case STATE_WAITING:
        if (entryCondition) {
            td.state.set('strategyState', STATE_ENTERING);
            td.trade.buy({ reason: 'Entry signal' });
        }
        break;

    case STATE_ENTERING:
        if (td.position.hasPosition) {
            td.state.set('strategyState', STATE_IN_POSITION);
            td.state.set('entryTime', Date.now());
        }
        break;

    case STATE_IN_POSITION:
        if (exitCondition) {
            td.state.set('strategyState', STATE_EXITING);
            td.trade.close('Exit signal');
        }
        break;

    case STATE_EXITING:
        if (!td.position.hasPosition) {
            td.state.set('strategyState', STATE_WAITING);
        }
        break;
}

State Management Functions

Getting All State

const allState = td.state.getAll();
td.utils.log('Current state', allState);

Deleting Values

// Delete single key
td.state.delete('temporaryValue');

// Clear all state
td.state.clear();

Checking Existence

// Check if key exists
const value = td.state.get('myKey');
if (value !== undefined) {
    // Key exists
}

Best Practices

Initialize with Defaults

// Always provide sensible defaults
const tradeCount = td.state.get('tradeCount', 0);
const isFirstRun = td.state.get('initialized', false) === false;

if (isFirstRun) {
    td.utils.log('First strategy run');
    td.state.set('initialized', true);
}

Clean Up Stale State

// Periodically clean up old state
const lastCleanup = td.state.get('lastStateCleanup', 0);
const cleanupInterval = 24 * 60 * 60 * 1000; // 24 hours

if (Date.now() - lastCleanup > cleanupInterval) {
    // Clear old data
    td.state.delete('oldPriceHistory');
    td.state.delete('deprecatedValue');
    td.state.set('lastStateCleanup', Date.now());
    td.utils.log('State cleanup completed');
}

Keep State Small

// BAD - storing too much data
td.state.set('allCandles', td.market.candles); // Don't do this!

// GOOD - store only what you need
td.state.set('lastHigh', Math.max(...td.market.candles.slice(-5).map(c => c.high)));

Use Meaningful Keys

// BAD
td.state.set('x', 5);
td.state.set('temp', true);

// GOOD
td.state.set('consecutiveLosses', 5);
td.state.set('inCooldownPeriod', true);

Handle Missing State Gracefully

// Always handle undefined values
const lastPrice = td.state.get('lastPrice');
if (lastPrice === undefined) {
    // First run - initialize
    td.state.set('lastPrice', td.market.price);
    return; // Skip this spin
}

const priceChange = td.market.price - lastPrice;

State Size Limits

State storage has limits to ensure performance:

  • Total state size: 1 MB maximum
  • Individual value size: 100 KB maximum
  • Number of keys: 1000 maximum

If you exceed these limits, older or less-used values may be evicted.

Next Steps