A TypeScript utility class for managing cryptocurrency exchange account state in-memory. Designed for trading bots, portfolio trackers, and any application that needs to maintain real-time account state across positions, orders, and balances.
- Features
- Installation
- Best Practices
- Quick Start
- Core API
- Custom Metadata
- Persistence
- Running Examples
- Contributions & Thanks
- License
- Account Balance Tracking - Monitor wallet balance changes
- Position Management - Track long/short positions with real-time P&L
- Order State Management - Monitor active, filled, and cancelled orders
- Symbol-based Organization - Organize data by trading pairs
- Custom Metadata Storage - Store custom data per symbol/position
- Exchange Agnostic - Works with any exchange (Binance, Bybit, etc.)
- Persistence Ready - Built-in persistence hooks for custom storage
- TypeScript Support - Full type safety with generics
npm install accountstate
# or
yarn add accountstate
- Initial Sync: Always sync from REST API before starting WebSocket streams
- Reconnection Handling: Re-sync state after WebSocket reconnections
- Error Handling: Implement proper error handling for API calls
- Persistence: Use persistence for custom metadata that can't be restored
- Performance: Use the built-in filtering and sorting methods for efficiency
- Type Safety: Define custom metadata interfaces for better type safety
import { AccountStateStore } from 'accountstate';
// Create a new account state store
const accountState = new AccountStateStore();
// Set wallet balance
accountState.setWalletBalance(10000);
// Track a position
accountState.setActivePosition('BTCUSDT', 'LONG', {
symbol: 'BTCUSDT',
timestampMs: Date.now(),
positionSide: 'LONG',
orderPositionSide: 'LONG',
positionPrice: 45000,
assetQty: 0.1,
value: 4500,
valueUpnl: 0,
liquidationPrice: 40000,
marginValue: 4500,
});
// Track an order
accountState.upsertActiveOrder({
exchangeOrderId: '12345',
customOrderId: 'my-order-1',
symbol: 'BTCUSDT',
orderSide: 'BUY',
orderType: 'LIMIT',
positionSide: 'LONG',
status: 'NEW',
price: 44000,
originalQuantity: 0.05,
executedQuantity: 0,
averagePrice: 0,
createdAtMs: Date.now(),
updatedAtMs: Date.now(),
isreduceOnly: false,
});
// Get account summary
const summary = accountState.getSessionSummary(10000);
console.log('Account Summary:', summary);
Or, check examples in the ./examples folder.
// Set and get wallet balance
accountState.setWalletBalance(10000);
const balance = accountState.getWalletBalance(); // 10000
// Track balance changes
accountState.storePreviousBalance();
accountState.setWalletBalance(10500);
const previousBalance = accountState.getPreviousBalance(); // 10000
// Check if symbol has any position
const hasPosition = accountState.isSymbolInAnyPosition('BTCUSDT');
// Check specific side position
const hasLongPosition = accountState.isSymbolSideInPosition('BTCUSDT', 'LONG');
// Get specific position
const longPosition = accountState.getActivePosition('BTCUSDT', 'LONG');
// Get all positions
const allPositions = accountState.getAllPositions();
// Get position counts
const { total, totalHedged } = accountState.getTotalActivePositions();
// Delete a position
accountState.deleteActivePosition('BTCUSDT', 'LONG');
// Get all orders
const allOrders = accountState.getOrders();
// Get active orders only
const activeOrders = accountState.getActiveOrders();
// Get orders for specific symbol
const btcOrders = accountState.getOrdersForSymbol('BTCUSDT');
// Get orders for symbol and side
const btcBuyOrders = accountState.getOrdersForSymbolSide('BTCUSDT', 'BUY');
// Get specific order
const order = accountState.getOrder('12345');
// Get orders by status
const newOrders = accountState.getOrdersByStatus('NEW');
// Get orders sorted by price
const ordersByPrice = accountState.getOrdersSortedByPrice(true); // ascending
// Clear all orders
accountState.clearAllOrders();
// Set symbol leverage
accountState.setSymbolLeverage('BTCUSDT', 10);
// Get symbol leverage
const leverage = accountState.getSymbolLeverage('BTCUSDT'); // 10
// Get all leverage settings
const allLeverage = accountState.getSymbolLeverageCache();
// Process price update to recalculate unrealized P&L
accountState.processPriceEvent({
symbol: 'BTCUSDT',
price: 46000,
timestamp: Date.now(),
});
// Get session summary with P&L calculations
const summary = accountState.getSessionSummary(startingBalance);
console.log('Realized P&L:', summary.account.pnlState.realisedPnl);
console.log('Unrealized P&L:', summary.account.pnlState.unrealisedPnl);
This storage class also supports per-symbol "metadata". This is a key:value object you can use to store any information related to that symbol's position.
This is typically custom data that an exchange might not have any knowledge of.
Some examples:
- How many entries have happened on the short side of a symbol's position.
- When did this position first open.
- What state is the trailing SL mechanism in.
- What price did the last position for this symbol close.
// Define your metadata type
interface MyPositionMetadata {
leaderId: string;
entryCount: number;
lastEntryPrice: number;
strategy: string;
}
// Create store with custom metadata type
const accountState = new AccountStateStore<MyPositionMetadata>();
// Set metadata for a symbol
accountState.setSymbolMetadata('BTCUSDT', {
leaderId: 'trader-123',
entryCount: 3,
lastEntryPrice: 45000,
strategy: 'DCA',
});
// Get metadata
const metadata = accountState.getSymbolMetadata('BTCUSDT');
// Update specific metadata value
accountState.setSymbolMetadataValue('BTCUSDT', 'entryCount', 4);
// Get all symbols with metadata
const symbolsWithMetadata = accountState.getSymbolsWithMetadata();
// Delete metadata
accountState.deletePositionMetadata('BTCUSDT');
The primary purpose of this module is to cache this state in-memory. Most of this can easily be fetched via the REST API, so persistence for the majority of this data is no concern.
However, the concept of per-symbol "metadata" is a custom one that cannot be easily restored once lost. If you use any of the metadata-related set/delete methods in the module, isPendingPersist()
will automatically be set to return true
.
This is a good way to check if there's a state change to persist somewhere, but it's up to you to implement the persistence mechanism based on your own needs. One way is to debounce an action to getAllSymbolMetadata()
, persist it somewhere, and finally call setIsPendingPersist(false)
.
There's no wrong way to do this. Here's a high level example that extends the account state store to automatically persist to Redis on a timer, if the stored metadata changed:
const PERSIST_ACCOUNT_POSITION_METADATA_EVERY_MS = 250;
export interface EnginePositionMetadata {
leaderId: string;
leaderName: string;
entryCountLong: number;
entryCountShort: number;
}
/**
* This abstraction layer extends the open source "account state store" class,
* adding a persistence mechanism so nothing is lost after restart.
*
* Data is stored in Redis, keyed by the accountId.
*
* The RedisPersistanceAPI is a custom implementation around the ioredis client.
*/
export class PersistedAccountStateStore extends AccountStateStore<EnginePositionMetadata> {
private redisAPI: RedisPersistanceAPI<'positionMetadata'>;
private didRestorePositionMetadata = false;
private accountId: string;
constructor(accountId: string, redisAPI: RedisPersistanceAPI) {
super();
this.redisAPI = redisAPI;
this.accountId = accountId;
/** Start the persistence timer and also fetch any initial state, if any is found **/
this.startPersistPositionMetadataTimer();
}
/** Call this during bootstrap to ensure we've rehydrated before resuming */
async restorePersistedData(): Promise<void> {
// Query persisted position metadata from redis
const storedDataResult = await this.redisAPI.fetchJSONForAccountKey(
'positionMetadata',
this.accountId,
);
if (storedDataResult?.data && typeof storedDataResult.data === 'object') {
this.setAllSymbolMetadata(storedDataResult.data);
} else {
console.log(
`No state data in redis for "${this.accountId}" - nothing to restore`,
);
}
// Overwrite local store with restored data
this.didRestorePositionMetadata = true;
}
private startPersistPositionMetadataTimer(): void {
setInterval(async () => {
if (!this.didRestorePositionMetadata) {
await this.restorePersistedData();
}
if (!this.isPendingPersist()) {
return;
}
try {
this.setIsPendingPersist(false);
await this.redisAPI.writeJSONForAccountKey(
'positionMetadata',
this.accountId,
this.getAllSymbolMetadata(),
);
console.log(`Saved position metadata to redis`);
} catch (e) {
console.error(
`Exception writing position metadata to redis: ${sanitiseError(e)}`,
);
this.setIsPendingPersist(true);
}
}, PERSIST_ACCOUNT_POSITION_METADATA_EVERY_MS);
}
}
The repository includes complete working examples for popular exchanges. You can find them in the ./examples folder.
-
Create
.env
file:BINANCE_API_KEY=your_api_key BINANCE_API_SECRET=your_api_secret
-
Run example:
tsx examples/binance-futures-usdm.ts
-
Create
.env
file:BYBIT_API_KEY=your_api_key BYBIT_API_SECRET=your_api_secret
-
Run example:
tsx examples/bybit-futures.ts
All examples demonstrate:
- Initial state synchronization from REST APIs
- Real-time updates via WebSocket
- Automatic reconnection handling
- State consistency maintenance
- Account summary reporting
Have my projects helped you? Share the love, there are many ways you can show your thanks:
- Star & share my projects.
- Are my projects useful? Sponsor me on Github and support my effort to maintain & improve them: https://github.com/sponsors/tiagosiebler
- Have an interesting project? Get in touch & invite me to it.
- Or buy me all the coffee:
- ETH(ERC20):
0xA3Bda8BecaB4DCdA539Dc16F9C54a592553Be06C
- ETH(ERC20):
Contributions are encouraged, I will review any incoming pull requests. See the issues tab for todo items.
Check out my related JavaScript/TypeScript/Node.js projects:
- Try my REST API & WebSocket SDKs:
- Try my misc utilities:
- Check out my examples:
MIT License - see LICENSE file for details.