@quantform/core 0.3.238 → 0.3.243

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 (53) hide show
  1. package/dist/adapter/adapter-aggregate.js +5 -5
  2. package/dist/adapter/adapter-aggregate.js.map +1 -1
  3. package/dist/adapter/paper/paper-adapter.js +2 -2
  4. package/dist/adapter/paper/paper-adapter.js.map +1 -1
  5. package/dist/bin.d.ts +0 -1
  6. package/dist/bin.js +1 -7
  7. package/dist/bin.js.map +1 -1
  8. package/dist/domain/asset.d.ts +3 -3
  9. package/dist/domain/asset.js +8 -8
  10. package/dist/domain/asset.js.map +1 -1
  11. package/dist/domain/asset.spec.js +4 -4
  12. package/dist/domain/asset.spec.js.map +1 -1
  13. package/dist/domain/instrument.d.ts +1 -1
  14. package/dist/domain/instrument.js +6 -6
  15. package/dist/domain/instrument.js.map +1 -1
  16. package/dist/domain/instrument.spec.js +7 -7
  17. package/dist/domain/instrument.spec.js.map +1 -1
  18. package/dist/domain/orderbook.d.ts +0 -1
  19. package/dist/ipc.d.ts +10 -5
  20. package/dist/ipc.js +40 -26
  21. package/dist/ipc.js.map +1 -1
  22. package/dist/ipc.spec.js +45 -1
  23. package/dist/ipc.spec.js.map +1 -1
  24. package/dist/session/session.d.ts +2 -2
  25. package/dist/session/session.js +9 -9
  26. package/dist/session/session.js.map +1 -1
  27. package/dist/shared/index.d.ts +1 -0
  28. package/dist/shared/index.js +1 -0
  29. package/dist/shared/index.js.map +1 -1
  30. package/dist/shared/task.d.ts +6 -0
  31. package/dist/shared/task.js +25 -0
  32. package/dist/shared/task.js.map +1 -0
  33. package/dist/store/event/store-instrument.event.js +1 -1
  34. package/dist/store/event/store-instrument.event.js.map +1 -1
  35. package/dist/store/event/store-order.event.js +0 -8
  36. package/dist/store/event/store-order.event.js.map +1 -1
  37. package/dist/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +2 -1
  39. package/src/adapter/adapter-aggregate.ts +5 -5
  40. package/src/adapter/paper/paper-adapter.ts +2 -2
  41. package/src/bin.ts +0 -11
  42. package/src/domain/asset.spec.ts +4 -4
  43. package/src/domain/asset.ts +9 -9
  44. package/src/domain/instrument.spec.ts +7 -7
  45. package/src/domain/instrument.ts +6 -6
  46. package/src/domain/orderbook.ts +1 -1
  47. package/src/ipc.spec.ts +59 -4
  48. package/src/ipc.ts +54 -24
  49. package/src/session/session.ts +10 -10
  50. package/src/shared/index.ts +1 -0
  51. package/src/shared/task.ts +30 -0
  52. package/src/store/event/store-instrument.event.ts +1 -1
  53. package/src/store/event/store-order.event.ts +0 -12
@@ -10,9 +10,9 @@ describe('instrument tests', () => {
10
10
  );
11
11
 
12
12
  expect(sut.base.name).toEqual('abc');
13
- expect(sut.base.exchange).toEqual('xyz');
13
+ expect(sut.base.adapter).toEqual('xyz');
14
14
  expect(sut.quote.name).toEqual('def');
15
- expect(sut.quote.exchange).toEqual('xyz');
15
+ expect(sut.quote.adapter).toEqual('xyz');
16
16
  expect(sut.toString()).toEqual('xyz:abc-def');
17
17
  });
18
18
  });
@@ -22,9 +22,9 @@ describe('instrument selector tests', () => {
22
22
  const sut = instrumentOf('xyz:abc-def');
23
23
 
24
24
  expect(sut.base.name).toEqual('abc');
25
- expect(sut.base.exchange).toEqual('xyz');
25
+ expect(sut.base.adapter).toEqual('xyz');
26
26
  expect(sut.quote.name).toEqual('def');
27
- expect(sut.quote.exchange).toEqual('xyz');
27
+ expect(sut.quote.adapter).toEqual('xyz');
28
28
  expect(sut.toString()).toEqual('xyz:abc-def');
29
29
  });
30
30
 
@@ -32,9 +32,9 @@ describe('instrument selector tests', () => {
32
32
  const sut = instrumentOf('XYZ:ABC-DEF');
33
33
 
34
34
  expect(sut.base.name).toEqual('abc');
35
- expect(sut.base.exchange).toEqual('xyz');
35
+ expect(sut.base.adapter).toEqual('xyz');
36
36
  expect(sut.quote.name).toEqual('def');
37
- expect(sut.quote.exchange).toEqual('xyz');
37
+ expect(sut.quote.adapter).toEqual('xyz');
38
38
  expect(sut.toString()).toEqual('xyz:abc-def');
39
39
  });
40
40
 
@@ -62,7 +62,7 @@ describe('instrument selector tests', () => {
62
62
  expect(fn).toThrow(Error);
63
63
  });
64
64
 
65
- test('should throw invalid format message for missing exchange name', () => {
65
+ test('should throw invalid format message for missing adapter name', () => {
66
66
  const fn = () => {
67
67
  assetOf(':abc-def');
68
68
  };
@@ -9,9 +9,9 @@ export class InstrumentSelector {
9
9
  readonly base: AssetSelector;
10
10
  readonly quote: AssetSelector;
11
11
 
12
- constructor(base: string, quote: string, exchange: string) {
13
- this.base = new AssetSelector(base.toLowerCase(), exchange.toLowerCase());
14
- this.quote = new AssetSelector(quote.toLowerCase(), exchange.toLowerCase());
12
+ constructor(base: string, quote: string, adapter: string) {
13
+ this.base = new AssetSelector(base.toLowerCase(), adapter.toLowerCase());
14
+ this.quote = new AssetSelector(quote.toLowerCase(), adapter.toLowerCase());
15
15
 
16
16
  this.id = `${this.base.toString()}-${this.quote.name}`;
17
17
  }
@@ -31,10 +31,10 @@ export class Instrument extends InstrumentSelector implements Component {
31
31
  leverage?: number = null;
32
32
 
33
33
  constructor(readonly base: Asset, readonly quote: Asset, readonly raw: string) {
34
- super(base.name, quote.name, base.exchange);
34
+ super(base.name, quote.name, base.adapter);
35
35
 
36
- if (base.exchange != quote.exchange) {
37
- throw new Error('Exchange mismatch!');
36
+ if (base.adapter != quote.adapter) {
37
+ throw new Error('Adapter mismatch!');
38
38
  }
39
39
  }
40
40
  }
@@ -11,7 +11,7 @@ export class Orderbook implements Component {
11
11
  bestAskQuantity: number;
12
12
  bestBidRate: number;
13
13
  bestBidQuantity: number;
14
- az;
14
+
15
15
  get midRate(): number {
16
16
  return this.instrument.quote.fixed((this.bestAskRate + this.bestBidRate) / 2);
17
17
  }
package/src/ipc.spec.ts CHANGED
@@ -3,14 +3,18 @@ import {
3
3
  Adapter,
4
4
  AdapterFeedCommand,
5
5
  AdapterAwakeCommand,
6
- AdapterAccountCommand
6
+ AdapterAccountCommand,
7
+ AdapterSubscribeCommand,
8
+ AdapterDisposeCommand
7
9
  } from './adapter';
8
10
  import { PaperAdapter, PaperSpotExecutor } from './adapter/paper';
9
11
  import { PaperExecutor } from './adapter/paper/executor/paper-executor';
10
- import { IpcFeedCommand, run } from './ipc';
12
+ import { run } from './ipc';
11
13
  import { Feed, InMemoryStorage } from './storage';
12
14
  import { instrumentOf } from './domain';
13
- import { handler } from './shared';
15
+ import { handler, task } from './shared';
16
+ import { EventEmitter } from 'events';
17
+ import { from, of, take, tap } from 'rxjs';
14
18
 
15
19
  class DefaultAdapter extends Adapter {
16
20
  name = 'default';
@@ -26,6 +30,12 @@ class DefaultAdapter extends Adapter {
26
30
  @handler(AdapterAwakeCommand)
27
31
  onAwake(command: AdapterAwakeCommand) {}
28
32
 
33
+ @handler(AdapterDisposeCommand)
34
+ onDispose(command: AdapterDisposeCommand) {}
35
+
36
+ @handler(AdapterSubscribeCommand)
37
+ onSubscribe(command: AdapterSubscribeCommand) {}
38
+
29
39
  @handler(AdapterAccountCommand)
30
40
  onAccount(command: AdapterAccountCommand) {}
31
41
 
@@ -51,6 +61,51 @@ describe('ipc feed tests', () => {
51
61
  command
52
62
  );
53
63
 
54
- expect(session.descriptor).toBeUndefined();
64
+ //expect(session.descriptor).toBeUndefined();
65
+ });
66
+
67
+ test('should dispatch session started event', done => {
68
+ const command = {
69
+ type: 'paper',
70
+ balance: { 'default:usd': 100 }
71
+ };
72
+
73
+ const ipcSub = new EventEmitter();
74
+
75
+ ipcSub.on('message', (message: any) => {
76
+ expect(message.type).toBe('paper:started');
77
+ done();
78
+ });
79
+
80
+ run(
81
+ {
82
+ adapter: [new DefaultAdapter()],
83
+ describe: (session: Session) => session.trade(instrumentOf('default:btc-usdt')),
84
+ ipcSub
85
+ },
86
+ command
87
+ );
88
+ });
89
+
90
+ test('should execute user task', done => {
91
+ task('hello-world', session => {
92
+ return of(1).pipe(
93
+ take(1),
94
+ tap(() => done())
95
+ );
96
+ });
97
+
98
+ const command = {
99
+ type: 'task',
100
+ taskName: 'hello-world'
101
+ };
102
+
103
+ run(
104
+ {
105
+ adapter: [new DefaultAdapter()],
106
+ describe: (session: Session) => session.trade(instrumentOf('default:btc-usdt'))
107
+ },
108
+ command
109
+ );
55
110
  });
56
111
  });
package/src/ipc.ts CHANGED
@@ -2,10 +2,15 @@ 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 { EventEmitter } from 'events';
8
9
  import minimist = require('minimist');
10
+ import dotenv = require('dotenv');
11
+
12
+ // force to load environment variables from .env file if this file imported.
13
+ dotenv.config();
9
14
 
10
15
  /**
11
16
  * Base command/query interface for IPC communication.
@@ -73,12 +78,6 @@ export class IpcBacktestCommand implements IpcCommand {
73
78
  balance: { [key: string]: number };
74
79
  }
75
80
 
76
- @event
77
- export class IpcUniverseQuery implements IpcCommand {
78
- type = 'universe';
79
- exchange: string;
80
- }
81
-
82
81
  /**
83
82
  * Feeds specific session descriptor with instrument data.
84
83
  */
@@ -102,6 +101,19 @@ export class IpcFeedCommand implements IpcCommand {
102
101
  to: number;
103
102
  }
104
103
 
104
+ /**
105
+ * Executes user task defined in quantform.ts file.
106
+ */
107
+ @event
108
+ export class IpcTaskCommand implements IpcCommand {
109
+ type = 'task';
110
+
111
+ /**
112
+ * Name of the task to execute.
113
+ */
114
+ taskName: string;
115
+ }
116
+
105
117
  /**
106
118
  * Stores current session instance.
107
119
  */
@@ -109,11 +121,13 @@ class IpcSessionAccessor {
109
121
  session: Session;
110
122
  }
111
123
 
124
+ export declare type SessionRunDescriptor = SessionDescriptor & { ipcSub?: EventEmitter };
125
+
112
126
  /**
113
127
  * Inter process communication handler.
114
128
  */
115
129
  class IpcHandler extends Topic<{ type: string }, IpcSessionAccessor> {
116
- constructor(private readonly descriptor: SessionDescriptor) {
130
+ constructor(private readonly descriptor: SessionRunDescriptor) {
117
131
  super();
118
132
  }
119
133
 
@@ -209,28 +223,20 @@ class IpcHandler extends Topic<{ type: string }, IpcSessionAccessor> {
209
223
  });
210
224
  }
211
225
 
212
- /**
213
- * @see IpcUniverseQuery
214
- */
215
- @handler(IpcUniverseQuery)
216
- onUniverse(query: IpcUniverseQuery, accessor: IpcSessionAccessor) {
217
- accessor.session = accessor.session ?? idle(this.descriptor);
218
- }
219
-
220
226
  /**
221
227
  * @see IpcFeedCommand
222
228
  */
223
229
  @handler(IpcFeedCommand)
224
230
  async onFeed(command: IpcFeedCommand, accessor: IpcSessionAccessor) {
225
- accessor.session = accessor.session ?? idle(this.descriptor);
231
+ accessor.session = accessor.session ?? live(this.descriptor);
226
232
  const instrument = instrumentOf(command.instrument);
227
233
 
228
- await accessor.session.awake();
234
+ await accessor.session.awake(true);
229
235
 
230
236
  this.notify({ type: 'feed:started' });
231
237
 
232
238
  await accessor.session.aggregate.dispatch(
233
- instrument.base.exchange,
239
+ instrument.base.adapter,
234
240
  new AdapterFeedCommand(
235
241
  instrument,
236
242
  command.from,
@@ -251,15 +257,39 @@ class IpcHandler extends Topic<{ type: string }, IpcSessionAccessor> {
251
257
  await accessor.session.dispose();
252
258
  }
253
259
 
260
+ /**
261
+ * @see IpcTaskCommand
262
+ */
263
+ @handler(IpcTaskCommand)
264
+ async onTask(query: IpcTaskCommand, accessor: IpcSessionAccessor) {
265
+ accessor.session = accessor.session ?? live(this.descriptor);
266
+
267
+ await accessor.session.awake(true);
268
+
269
+ this.notify({ type: 'task:started', taskName: query.taskName });
270
+
271
+ let result = undefined;
272
+
273
+ try {
274
+ result = await runTask(query.taskName, accessor.session);
275
+ } catch (e) {
276
+ result = e;
277
+ }
278
+
279
+ this.notify({ type: 'task:completed', taskName: query.taskName, result });
280
+
281
+ await accessor.session.dispose();
282
+ }
283
+
254
284
  /**
255
285
  * Sends a message to parent process.
256
286
  */
257
287
  private notify(message: any) {
258
- if (!process.send) {
259
- return;
288
+ if (process.send) {
289
+ process.send(message);
260
290
  }
261
291
 
262
- process.send(message);
292
+ this.descriptor.ipcSub?.emit('message', message);
263
293
  }
264
294
  }
265
295
 
@@ -270,7 +300,7 @@ class IpcHandler extends Topic<{ type: string }, IpcSessionAccessor> {
270
300
  * @returns new session.
271
301
  */
272
302
  export async function run(
273
- descriptor: SessionDescriptor,
303
+ descriptor: SessionRunDescriptor,
274
304
  ...commands: IpcCommand[]
275
305
  ): Promise<Session> {
276
306
  const handler = new IpcHandler(descriptor);
@@ -57,7 +57,7 @@ export class Session {
57
57
  }
58
58
  }
59
59
 
60
- async awake(): Promise<void> {
60
+ async awake(idle: boolean = false): Promise<void> {
61
61
  if (this.initialized) {
62
62
  return;
63
63
  }
@@ -67,7 +67,7 @@ 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) {
70
+ if (!idle && this.descriptor?.describe) {
71
71
  this.subscription = this.descriptor.describe(this).subscribe();
72
72
  }
73
73
  }
@@ -151,12 +151,12 @@ export class Session {
151
151
  const grouped = instrument
152
152
  .filter(it => it != null)
153
153
  .reduce((aggregate, it) => {
154
- const exchange = it.base.exchange;
154
+ const adapter = it.base.adapter;
155
155
 
156
- if (aggregate[exchange]) {
157
- aggregate[exchange].push(it);
156
+ if (aggregate[adapter]) {
157
+ aggregate[adapter].push(it);
158
158
  } else {
159
- aggregate[exchange] = [it];
159
+ aggregate[adapter] = [it];
160
160
  }
161
161
 
162
162
  return aggregate;
@@ -176,7 +176,7 @@ export class Session {
176
176
  await Promise.all(
177
177
  orders.map(it =>
178
178
  this.aggregate.dispatch<AdapterOrderOpenCommand, void>(
179
- it.instrument.base.exchange,
179
+ it.instrument.base.adapter,
180
180
  new AdapterOrderOpenCommand(it)
181
181
  )
182
182
  )
@@ -188,7 +188,7 @@ export class Session {
188
188
  */
189
189
  cancel(order: Order): Promise<void> {
190
190
  return this.aggregate.dispatch(
191
- order.instrument.base.exchange,
191
+ order.instrument.base.adapter,
192
192
  new AdapterOrderCancelCommand(order)
193
193
  );
194
194
  }
@@ -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(
@@ -375,7 +375,7 @@ export class Session {
375
375
  switchMap(() =>
376
376
  from(
377
377
  this.aggregate.dispatch<AdapterHistoryQuery, Candle[]>(
378
- selector.base.exchange,
378
+ selector.base.adapter,
379
379
  new AdapterHistoryQuery(selector, timeframe, length)
380
380
  )
381
381
  )
@@ -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
+ }
@@ -23,7 +23,7 @@ export function InstrumentPatchEventHandler(event: InstrumentPatchEvent, state:
23
23
  const selector = new InstrumentSelector(
24
24
  event.base.name,
25
25
  event.quote.name,
26
- event.base.exchange
26
+ event.base.adapter
27
27
  );
28
28
 
29
29
  let instrument = state.universe.instrument[selector.toString()];
@@ -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
  }