@quantform/core 0.3.241 → 0.3.247

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/adapter/backtester/backtester-adapter.d.ts +1 -2
  2. package/dist/adapter/backtester/backtester-adapter.js.map +1 -1
  3. package/dist/adapter/backtester/backtester-streamer.d.ts +2 -1
  4. package/dist/adapter/backtester/backtester-streamer.js +8 -7
  5. package/dist/adapter/backtester/backtester-streamer.js.map +1 -1
  6. package/dist/adapter/backtester/backtester-streamer.spec.js +9 -10
  7. package/dist/adapter/backtester/backtester-streamer.spec.js.map +1 -1
  8. package/dist/bin.d.ts +3 -5
  9. package/dist/bin.js +6 -12
  10. package/dist/bin.js.map +1 -1
  11. package/dist/ipc.d.ts +10 -16
  12. package/dist/ipc.js +82 -67
  13. package/dist/ipc.js.map +1 -1
  14. package/dist/ipc.spec.js +24 -8
  15. package/dist/ipc.spec.js.map +1 -1
  16. package/dist/session/session-descriptor.d.ts +6 -3
  17. package/dist/session/session.d.ts +2 -2
  18. package/dist/session/session.js +3 -4
  19. package/dist/session/session.js.map +1 -1
  20. package/dist/session/session.spec.js +1 -5
  21. package/dist/session/session.spec.js.map +1 -1
  22. package/dist/shared/index.d.ts +1 -0
  23. package/dist/shared/index.js +1 -0
  24. package/dist/shared/index.js.map +1 -1
  25. package/dist/shared/task.d.ts +6 -0
  26. package/dist/shared/task.js +25 -0
  27. package/dist/shared/task.js.map +1 -0
  28. package/dist/store/event/store-order.event.js +0 -8
  29. package/dist/store/event/store-order.event.js.map +1 -1
  30. package/dist/tests/backtester-adapter.spec.js +7 -8
  31. package/dist/tests/backtester-adapter.spec.js.map +1 -1
  32. package/dist/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +2 -1
  34. package/src/adapter/backtester/backtester-adapter.ts +2 -3
  35. package/src/adapter/backtester/backtester-streamer.spec.ts +10 -6
  36. package/src/adapter/backtester/backtester-streamer.ts +8 -7
  37. package/src/bin.ts +17 -18
  38. package/src/ipc.spec.ts +36 -8
  39. package/src/ipc.ts +98 -87
  40. package/src/session/session-descriptor.ts +7 -4
  41. package/src/session/session.spec.ts +1 -5
  42. package/src/session/session.ts +4 -4
  43. package/src/shared/index.ts +1 -0
  44. package/src/shared/task.ts +30 -0
  45. package/src/store/event/store-order.event.ts +0 -12
  46. package/src/tests/backtester-adapter.spec.ts +11 -7
package/src/bin.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  BacktesterAdapter,
3
- BacktesterOptions,
3
+ BacktesterListener,
4
4
  BacktesterStreamer
5
5
  } from './adapter/backtester';
6
6
  import { AdapterAggregate } from './adapter';
7
- import { PaperAdapter, PaperOptions } from './adapter/paper';
7
+ import { PaperAdapter } from './adapter/paper';
8
8
  import { Session, SessionDescriptor } from './session';
9
9
  import { Store } from './store';
10
10
 
@@ -16,15 +16,25 @@ import { Store } from './store';
16
16
  */
17
17
  export function backtest(
18
18
  descriptor: SessionDescriptor,
19
- options: BacktesterOptions
19
+ listener?: BacktesterListener
20
20
  ): [Session, BacktesterStreamer] {
21
21
  const store = new Store();
22
22
 
23
- const streamer = new BacktesterStreamer(store, descriptor.feed, options);
23
+ const streamer = new BacktesterStreamer(
24
+ store,
25
+ descriptor.feed,
26
+ descriptor.options.backtester,
27
+ listener
28
+ );
24
29
  const aggregate = new AdapterAggregate(
25
30
  store,
26
31
  descriptor.adapter.map(
27
- it => new PaperAdapter(new BacktesterAdapter(it, streamer), store, options)
32
+ it =>
33
+ new PaperAdapter(
34
+ new BacktesterAdapter(it, streamer),
35
+ store,
36
+ descriptor.options.backtester
37
+ )
28
38
  )
29
39
  );
30
40
 
@@ -37,12 +47,12 @@ export function backtest(
37
47
  * @param options backtest options.
38
48
  * @returns new session object.
39
49
  */
40
- export function paper(descriptor: SessionDescriptor, options: PaperOptions): Session {
50
+ export function paper(descriptor: SessionDescriptor): Session {
41
51
  const store = new Store();
42
52
 
43
53
  const aggregate = new AdapterAggregate(
44
54
  store,
45
- descriptor.adapter.map(it => new PaperAdapter(it, store, options))
55
+ descriptor.adapter.map(it => new PaperAdapter(it, store, descriptor.options.paper))
46
56
  );
47
57
 
48
58
  return new Session(store, aggregate, descriptor);
@@ -59,14 +69,3 @@ export function live(descriptor: SessionDescriptor): Session {
59
69
 
60
70
  return new Session(store, aggregate, descriptor);
61
71
  }
62
-
63
- /**
64
- * Starts a new idle session.
65
- * @param descriptor session descriptor.
66
- */
67
- export function idle(descriptor: SessionDescriptor): Session {
68
- const store = new Store();
69
- const aggregate = new AdapterAggregate(store, descriptor.adapter);
70
-
71
- return new Session(store, aggregate);
72
- }
package/src/ipc.spec.ts CHANGED
@@ -9,10 +9,12 @@ import {
9
9
  } from './adapter';
10
10
  import { PaperAdapter, PaperSpotExecutor } from './adapter/paper';
11
11
  import { PaperExecutor } from './adapter/paper/executor/paper-executor';
12
- import { ipcSub, run } from './ipc';
12
+ import { run } from './ipc';
13
13
  import { Feed, InMemoryStorage } from './storage';
14
14
  import { instrumentOf } from './domain';
15
- import { handler } from './shared';
15
+ import { handler, task } from './shared';
16
+ import { EventEmitter } from 'events';
17
+ import { from, of, take, tap } from 'rxjs';
16
18
 
17
19
  class DefaultAdapter extends Adapter {
18
20
  name = 'default';
@@ -53,21 +55,21 @@ describe('ipc feed tests', () => {
53
55
  const session = await run(
54
56
  {
55
57
  adapter: [new DefaultAdapter()],
56
- feed: new Feed(new InMemoryStorage()),
57
- describe: (session: Session) => session.trade(instrumentOf('default:btc-usdt'))
58
+ feed: new Feed(new InMemoryStorage())
58
59
  },
59
60
  command
60
61
  );
61
62
 
62
- expect(session.descriptor).toBeUndefined();
63
+ //expect(session.descriptor).toBeUndefined();
63
64
  });
64
65
 
65
66
  test('should dispatch session started event', done => {
66
67
  const command = {
67
- type: 'paper',
68
- balance: { 'default:usd': 100 }
68
+ type: 'paper'
69
69
  };
70
70
 
71
+ const ipcSub = new EventEmitter();
72
+
71
73
  ipcSub.on('message', (message: any) => {
72
74
  expect(message.type).toBe('paper:started');
73
75
  done();
@@ -76,7 +78,33 @@ describe('ipc feed tests', () => {
76
78
  run(
77
79
  {
78
80
  adapter: [new DefaultAdapter()],
79
- describe: (session: Session) => session.trade(instrumentOf('default:btc-usdt'))
81
+ ipcSub,
82
+ options: {
83
+ paper: {
84
+ balance: { 'default:usd': 100 }
85
+ }
86
+ }
87
+ },
88
+ command
89
+ );
90
+ });
91
+
92
+ test('should execute user task', done => {
93
+ task('hello-world', session => {
94
+ return of(1).pipe(
95
+ take(1),
96
+ tap(() => done())
97
+ );
98
+ });
99
+
100
+ const command = {
101
+ type: 'task',
102
+ taskName: 'hello-world'
103
+ };
104
+
105
+ run(
106
+ {
107
+ adapter: [new DefaultAdapter()]
80
108
  },
81
109
  command
82
110
  );
package/src/ipc.ts CHANGED
@@ -2,13 +2,17 @@ import { AdapterFeedCommand } from './adapter';
2
2
  import { Session, SessionDescriptor } from './session';
3
3
  import { instrumentOf } from './domain';
4
4
  import { Topic, event, handler } from './shared/topic';
5
- import { Logger } from './shared';
6
- import { backtest, idle, live, paper } from './bin';
5
+ import { runTask, Logger } from './shared';
6
+ import { backtest, live, paper } from './bin';
7
7
  import { BacktesterStreamer } from './adapter/backtester';
8
+ import { Observable } from 'rxjs';
8
9
  import { EventEmitter } from 'events';
10
+ import { join } from 'path';
9
11
  import minimist = require('minimist');
12
+ import dotenv = require('dotenv');
10
13
 
11
- export const ipcSub = new EventEmitter();
14
+ // force to load environment variables from .env file if this file imported.
15
+ dotenv.config();
12
16
 
13
17
  /**
14
18
  * Base command/query interface for IPC communication.
@@ -44,12 +48,6 @@ export class IpcPaperCommand implements IpcCommand {
44
48
  * The optional session identifier.
45
49
  */
46
50
  id?: number;
47
-
48
- /**
49
- * Specifies trading balance, for example:
50
- * { "binance:usdt": 1000 }
51
- */
52
- balance: { [key: string]: number };
53
51
  }
54
52
 
55
53
  /**
@@ -58,28 +56,6 @@ export class IpcPaperCommand implements IpcCommand {
58
56
  @event
59
57
  export class IpcBacktestCommand implements IpcCommand {
60
58
  type = 'backtest';
61
-
62
- /**
63
- * Start date of the backtest in unix timestamp.
64
- */
65
- from: number;
66
-
67
- /**
68
- * Due date of the backtest in unix timestamp.
69
- */
70
- to: number;
71
-
72
- /**
73
- * Specifies trading balance, for example:
74
- * { "binance:usdt": 1000 }
75
- */
76
- balance: { [key: string]: number };
77
- }
78
-
79
- @event
80
- export class IpcUniverseQuery implements IpcCommand {
81
- type = 'universe';
82
- adapter: string;
83
59
  }
84
60
 
85
61
  /**
@@ -97,12 +73,25 @@ export class IpcFeedCommand implements IpcCommand {
97
73
  /**
98
74
  * Start date of the feed in unix timestamp.
99
75
  */
100
- from: number;
76
+ from?: number;
101
77
 
102
78
  /**
103
79
  * Due date of the feed in unix timestamp.
104
80
  */
105
- to: number;
81
+ to?: number;
82
+ }
83
+
84
+ /**
85
+ * Executes user task defined in quantform.ts file.
86
+ */
87
+ @event
88
+ export class IpcTaskCommand implements IpcCommand {
89
+ type = 'task';
90
+
91
+ /**
92
+ * Name of the task to execute.
93
+ */
94
+ taskName: string;
106
95
  }
107
96
 
108
97
  /**
@@ -112,11 +101,13 @@ class IpcSessionAccessor {
112
101
  session: Session;
113
102
  }
114
103
 
104
+ export declare type IpcSessionDescriptor = SessionDescriptor & { ipcSub?: EventEmitter };
105
+
115
106
  /**
116
107
  * Inter process communication handler.
117
108
  */
118
109
  class IpcHandler extends Topic<{ type: string }, IpcSessionAccessor> {
119
- constructor(private readonly descriptor: SessionDescriptor) {
110
+ constructor(private readonly descriptor: IpcSessionDescriptor) {
120
111
  super();
121
112
  }
122
113
 
@@ -136,7 +127,7 @@ class IpcHandler extends Topic<{ type: string }, IpcSessionAccessor> {
136
127
  session: accessor.session.descriptor?.id
137
128
  });
138
129
 
139
- await accessor.session.awake();
130
+ await accessor.session.awake(this.describe());
140
131
  }
141
132
 
142
133
  /**
@@ -148,16 +139,14 @@ class IpcHandler extends Topic<{ type: string }, IpcSessionAccessor> {
148
139
  this.descriptor.id = command.id;
149
140
  }
150
141
 
151
- accessor.session = paper(this.descriptor, {
152
- balance: command.balance
153
- });
142
+ accessor.session = paper(this.descriptor);
154
143
 
155
144
  this.notify({
156
145
  type: 'paper:started',
157
146
  session: accessor.session.descriptor?.id
158
147
  });
159
148
 
160
- await accessor.session.awake();
149
+ await accessor.session.awake(this.describe());
161
150
  }
162
151
 
163
152
  /**
@@ -167,68 +156,55 @@ class IpcHandler extends Topic<{ type: string }, IpcSessionAccessor> {
167
156
  onBacktestMode(command: IpcBacktestCommand, accessor: IpcSessionAccessor) {
168
157
  return new Promise<void>(async resolve => {
169
158
  const [session, streamer] = backtest(this.descriptor, {
170
- from: command.from,
171
- to: command.to,
172
- balance: command.balance,
173
- listener: {
174
- onBacktestStarted: (streamer: BacktesterStreamer) => {
175
- this.notify({
176
- type: 'backtest:started',
177
- session: session.descriptor?.id,
178
- timestamp: streamer.timestamp,
179
- from: command.from,
180
- to: command.to
181
- });
182
- },
183
- onBacktestUpdated: (streamer: BacktesterStreamer) => {
184
- this.notify({
185
- type: 'backtest:updated',
186
- session: session.descriptor?.id,
187
- timestamp: streamer.timestamp,
188
- from: command.from,
189
- to: command.to
190
- });
191
- },
192
- onBacktestCompleted: async (streamer: BacktesterStreamer) => {
193
- await accessor.session.dispose();
194
-
195
- this.notify({
196
- type: 'backtest:completed',
197
- session: session.descriptor?.id,
198
- timestamp: streamer.timestamp,
199
- from: command.from,
200
- to: command.to
201
- });
202
-
203
- resolve();
204
- }
159
+ onBacktestStarted: (streamer: BacktesterStreamer) => {
160
+ this.notify({
161
+ type: 'backtest:started',
162
+ session: session.descriptor?.id,
163
+ timestamp: streamer.timestamp,
164
+ from: this.descriptor.options.backtester.from,
165
+ to: this.descriptor.options.backtester.to
166
+ });
167
+ },
168
+ onBacktestUpdated: (streamer: BacktesterStreamer) => {
169
+ this.notify({
170
+ type: 'backtest:updated',
171
+ session: session.descriptor?.id,
172
+ timestamp: streamer.timestamp,
173
+ from: this.descriptor.options.backtester.from,
174
+ to: this.descriptor.options.backtester.to
175
+ });
176
+ },
177
+ onBacktestCompleted: async (streamer: BacktesterStreamer) => {
178
+ await accessor.session.dispose();
179
+
180
+ this.notify({
181
+ type: 'backtest:completed',
182
+ session: session.descriptor?.id,
183
+ timestamp: streamer.timestamp,
184
+ from: this.descriptor.options.backtester.from,
185
+ to: this.descriptor.options.backtester.to
186
+ });
187
+
188
+ resolve();
205
189
  }
206
190
  });
207
191
 
208
192
  accessor.session = session;
209
193
 
210
- await accessor.session.awake();
194
+ await accessor.session.awake(this.describe());
211
195
  await streamer.tryContinue().catch(it => Logger.error(it));
212
196
  });
213
197
  }
214
198
 
215
- /**
216
- * @see IpcUniverseQuery
217
- */
218
- @handler(IpcUniverseQuery)
219
- onUniverse(query: IpcUniverseQuery, accessor: IpcSessionAccessor) {
220
- accessor.session = accessor.session ?? idle(this.descriptor);
221
- }
222
-
223
199
  /**
224
200
  * @see IpcFeedCommand
225
201
  */
226
202
  @handler(IpcFeedCommand)
227
203
  async onFeed(command: IpcFeedCommand, accessor: IpcSessionAccessor) {
228
- accessor.session = accessor.session ?? idle(this.descriptor);
204
+ accessor.session = accessor.session ?? live(this.descriptor);
229
205
  const instrument = instrumentOf(command.instrument);
230
206
 
231
- await accessor.session.awake();
207
+ await accessor.session.awake(undefined);
232
208
 
233
209
  this.notify({ type: 'feed:started' });
234
210
 
@@ -254,6 +230,41 @@ class IpcHandler extends Topic<{ type: string }, IpcSessionAccessor> {
254
230
  await accessor.session.dispose();
255
231
  }
256
232
 
233
+ /**
234
+ * @see IpcTaskCommand
235
+ */
236
+ @handler(IpcTaskCommand)
237
+ async onTask(query: IpcTaskCommand, accessor: IpcSessionAccessor) {
238
+ accessor.session = accessor.session ?? live(this.descriptor);
239
+
240
+ await accessor.session.awake(undefined);
241
+
242
+ this.notify({ type: 'task:started', taskName: query.taskName });
243
+
244
+ let result = undefined;
245
+
246
+ try {
247
+ result = await runTask(query.taskName, accessor.session);
248
+ } catch (e) {
249
+ result = e;
250
+ }
251
+
252
+ this.notify({ type: 'task:completed', taskName: query.taskName, result });
253
+
254
+ await accessor.session.dispose();
255
+ }
256
+
257
+ describe(): (session: Session) => Observable<any> {
258
+ const pkg = require(join(process.cwd(), 'package.json'));
259
+ const describe = require(join(process.cwd(), pkg.main))?.default;
260
+
261
+ if (describe instanceof Function) {
262
+ return describe;
263
+ }
264
+
265
+ return undefined;
266
+ }
267
+
257
268
  /**
258
269
  * Sends a message to parent process.
259
270
  */
@@ -262,7 +273,7 @@ class IpcHandler extends Topic<{ type: string }, IpcSessionAccessor> {
262
273
  process.send(message);
263
274
  }
264
275
 
265
- ipcSub.emit('message', message);
276
+ this.descriptor.ipcSub?.emit('message', message);
266
277
  }
267
278
  }
268
279
 
@@ -273,7 +284,7 @@ class IpcHandler extends Topic<{ type: string }, IpcSessionAccessor> {
273
284
  * @returns new session.
274
285
  */
275
286
  export async function run(
276
- descriptor: SessionDescriptor,
287
+ descriptor: IpcSessionDescriptor,
277
288
  ...commands: IpcCommand[]
278
289
  ): Promise<Session> {
279
290
  const handler = new IpcHandler(descriptor);
@@ -1,8 +1,8 @@
1
1
  import { Adapter } from '../adapter';
2
- import { Session } from '.';
3
2
  import { Measurement } from '../storage/measurement';
4
3
  import { Feed } from '../storage';
5
- import { Observable } from 'rxjs';
4
+ import { BacktesterOptions } from './../adapter/backtester';
5
+ import { PaperOptions } from './../adapter/paper';
6
6
 
7
7
  /**
8
8
  * Describes a single session.
@@ -44,7 +44,10 @@ export interface SessionDescriptor {
44
44
  measurement?: Measurement;
45
45
 
46
46
  /**
47
- * Describes your trading strategy.
47
+ * Session additional options.
48
48
  */
49
- describe?: (session: Session) => Observable<any>;
49
+ options?: {
50
+ backtester?: BacktesterOptions;
51
+ paper?: PaperOptions;
52
+ };
50
53
  }
@@ -13,11 +13,7 @@ describe('session tests', () => {
13
13
  };
14
14
 
15
15
  test('should trigger once', done => {
16
- const session = paper(descriptor, {
17
- balance: {
18
- ['binance:btc']: 1.23
19
- }
20
- });
16
+ const session = paper(descriptor);
21
17
 
22
18
  session.instruments().subscribe({
23
19
  next: it => {
@@ -57,7 +57,7 @@ export class Session {
57
57
  }
58
58
  }
59
59
 
60
- async awake(): Promise<void> {
60
+ async awake(describe: (session: Session) => Observable<any>): Promise<void> {
61
61
  if (this.initialized) {
62
62
  return;
63
63
  }
@@ -67,8 +67,8 @@ export class Session {
67
67
  // awake all adapters and synchronize trading accounts with store.
68
68
  await this.aggregate.awake(this.descriptor != null);
69
69
 
70
- if (this.descriptor?.describe) {
71
- this.subscription = this.descriptor.describe(this).subscribe();
70
+ if (describe) {
71
+ this.subscription = describe(this).subscribe();
72
72
  }
73
73
  }
74
74
 
@@ -352,7 +352,7 @@ export class Session {
352
352
  );
353
353
  }
354
354
 
355
- balance(selector?: AssetSelector): Observable<Balance> {
355
+ balance(selector: AssetSelector): Observable<Balance> {
356
356
  return this.store.changes$.pipe(
357
357
  startWith(selector ? this.store.snapshot.balance[selector.toString()] : null),
358
358
  filter(
@@ -5,3 +5,4 @@ export * from './io';
5
5
  export * from './topic';
6
6
  export * from './policy';
7
7
  export * from './worker';
8
+ export * from './task';
@@ -0,0 +1,30 @@
1
+ import { lastValueFrom, Observable } from 'rxjs';
2
+ import { Session } from './../session';
3
+
4
+ declare type Task = (session: Session) => Observable<any> | Promise<any> | any;
5
+
6
+ const tasks: Record<string, Task> = {};
7
+
8
+ export function task(name: string, fn: Task) {
9
+ tasks[name] = fn;
10
+ }
11
+
12
+ export async function runTask(name: string, session: Session): Promise<void> {
13
+ const task = tasks[name];
14
+
15
+ if (!task) {
16
+ throw new Error('Unknown task: ' + name);
17
+ }
18
+
19
+ const result = tasks[name](session);
20
+
21
+ if (result instanceof Observable) {
22
+ return lastValueFrom(result);
23
+ }
24
+
25
+ if (result instanceof Promise) {
26
+ return await result;
27
+ }
28
+
29
+ return result;
30
+ }
@@ -12,12 +12,6 @@ export class OrderLoadEvent implements StoreEvent {
12
12
  }
13
13
 
14
14
  export function OrderLoadEventHandler(event: OrderLoadEvent, state: State) {
15
- const instrumentKey = event.order.instrument.toString();
16
-
17
- if (!(instrumentKey in state.subscription.instrument)) {
18
- throw new Error(`Trying to load order for unsubscribed instrument: ${instrumentKey}`);
19
- }
20
-
21
15
  event.order.timestamp = event.timestamp;
22
16
 
23
17
  switch (event.order.state) {
@@ -48,12 +42,6 @@ export class OrderNewEvent implements StoreEvent {
48
42
  }
49
43
 
50
44
  export function OrderNewEventHandler(event: OrderNewEvent, state: State) {
51
- const instrumentKey = event.order.instrument.toString();
52
-
53
- if (!(instrumentKey in state.subscription.instrument)) {
54
- throw new Error(`Trying to order for unsubscribed instrument: ${instrumentKey}`);
55
- }
56
-
57
45
  if (event.order.state != 'NEW') {
58
46
  throw new Error(`Order is not new`);
59
47
  }
@@ -63,13 +63,17 @@ describe('backtester adapter tests', () => {
63
63
  });
64
64
 
65
65
  test('should stream data from input array', done => {
66
- const streamer = new BacktesterStreamer(store, feed, {
67
- balance: {
68
- ['binance:usdt']: 1000
66
+ const streamer = new BacktesterStreamer(
67
+ store,
68
+ feed,
69
+ {
70
+ balance: {
71
+ ['binance:usdt']: 1000
72
+ },
73
+ from: 0,
74
+ to: 100
69
75
  },
70
- from: 0,
71
- to: 100,
72
- listener: {
76
+ {
73
77
  onBacktestCompleted: () => {
74
78
  expect(store.snapshot.timestamp).toEqual(1);
75
79
  expect(store.snapshot.trade[instrument.toString()].rate).toEqual(100);
@@ -78,7 +82,7 @@ describe('backtester adapter tests', () => {
78
82
  done();
79
83
  }
80
84
  }
81
- });
85
+ );
82
86
 
83
87
  feed.save(instrument, [new TradePatchEvent(instrument, 100, 10, 1)]);
84
88