AccountState store
A simple utility class to store & manage simple account state in-memory.
The majority of account/position/trade/balance state is easily restorable and can be fetched via REST APIs after restarting a process.
This storage class is a way to cache this information in-memory, with utility functions to simplify accessing and manipulating that state.
$ npm install accountstate
# or
$ yarn add accountstate
TODO
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.
You can store anything custom here. However, if you do rely on this metadata,
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);
}
}