@backtest-kit/cli 0.0.1

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.
@@ -0,0 +1,1849 @@
1
+ import { Storage, Notification, Markdown, Report, StorageLive, StorageBacktest, NotificationLive, NotificationBacktest, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, listStrategySchema, overrideExchangeSchema, Backtest, Live, getCandles, checkCandles, warmCandles, listenRisk, listenSignal, listenStrategyCommit } from 'backtest-kit';
2
+ import { getErrorMessage, errorData, str, BehaviorSubject, singleshot, compose, execpool, queued, sleep, randomString, createAwaiter, TIMEOUT_SYMBOL, typo, retry, memoize } from 'functools-kit';
3
+ import fs, { constants } from 'fs';
4
+ import * as stackTrace from 'stack-trace';
5
+ import { fileURLToPath, pathToFileURL } from 'url';
6
+ import path from 'path';
7
+ import fs$1, { access } from 'fs/promises';
8
+ import dotenv from 'dotenv';
9
+ import { createActivator } from 'di-kit';
10
+ import ccxt from 'ccxt';
11
+ import { parseArgs } from 'util';
12
+ import { serve } from '@backtest-kit/ui';
13
+ import QuickChart from 'quickchart-js';
14
+ import { Telegraf, Input } from 'telegraf';
15
+ import imageSize from 'image-size';
16
+ import resizeImg from 'resize-image-buffer';
17
+ import { Readable } from 'stream';
18
+ import MarkdownIt from 'markdown-it';
19
+ import sanitizeHtml from 'sanitize-html';
20
+ import { JSDOM } from 'jsdom';
21
+ import Mustache from 'mustache';
22
+ import { createRequire } from 'module';
23
+
24
+ /**
25
+ * Fix for `Attempted to assign to readonly property (at redactToken)`
26
+ * @see https://github.com/telegraf/telegraf/issues/2078
27
+ */
28
+ {
29
+ const OriginalError = globalThis.Error;
30
+ globalThis.Error = new Proxy(OriginalError, {
31
+ construct(target, args) {
32
+ const instance = new target(...args);
33
+ let customMessage = instance.message;
34
+ const handler = {
35
+ get(target, prop, receiver) {
36
+ if (prop === "message") {
37
+ return customMessage;
38
+ }
39
+ return Reflect.get(target, prop, receiver);
40
+ },
41
+ set(target, prop, value, receiver) {
42
+ if (prop === "message") {
43
+ customMessage = value;
44
+ return true;
45
+ }
46
+ return Reflect.set(target, prop, value, receiver);
47
+ },
48
+ };
49
+ return new Proxy(instance, handler);
50
+ },
51
+ });
52
+ }
53
+
54
+ {
55
+ Storage.enable();
56
+ Notification.enable();
57
+ }
58
+ {
59
+ Markdown.disable();
60
+ Report.enable();
61
+ }
62
+ {
63
+ StorageLive.usePersist();
64
+ StorageBacktest.useDummy();
65
+ }
66
+ {
67
+ NotificationLive.useDummy();
68
+ NotificationBacktest.useDummy();
69
+ }
70
+
71
+ const ERROR_HANDLER_INSTALLED = Symbol.for("error-handler-installed");
72
+ function dumpStackTrace() {
73
+ const trace = stackTrace.get();
74
+ const result = [];
75
+ trace.forEach((callSite) => {
76
+ result.push(`File: ${callSite.getFileName()}`);
77
+ result.push(`Line: ${callSite.getLineNumber()}`);
78
+ result.push(`Function: ${callSite.getFunctionName() || "anonymous"}`);
79
+ result.push(`Method: ${callSite.getMethodName() || "none"}`);
80
+ result.push("---");
81
+ });
82
+ return str.newline(result);
83
+ }
84
+ const timeNow = () => {
85
+ const d = new Date();
86
+ const h = (d.getHours() < 10 ? "0" : "") + d.getHours();
87
+ const m = (d.getMinutes() < 10 ? "0" : "") + d.getMinutes();
88
+ return `${h}:${m}`;
89
+ };
90
+ class ErrorService {
91
+ constructor() {
92
+ this.handleGlobalError = async (error) => {
93
+ const today = new Date();
94
+ const dd = String(today.getDate()).padStart(2, "0");
95
+ const mm = String(today.getMonth() + 1).padStart(2, "0");
96
+ const yyyy = today.getFullYear();
97
+ const date = `${dd}/${mm}/${yyyy} ${timeNow()}`;
98
+ const msg = JSON.stringify({
99
+ message: getErrorMessage(error),
100
+ data: errorData(error),
101
+ }, null, 2);
102
+ const trace = dumpStackTrace();
103
+ fs.appendFileSync("./error.txt", `${date}\n${msg}\n${trace}\n\n`);
104
+ };
105
+ this._listenForError = () => {
106
+ process.on("uncaughtException", (err) => {
107
+ console.log(err);
108
+ this.handleGlobalError(err);
109
+ });
110
+ process.on("unhandledRejection", (err) => {
111
+ console.log(err);
112
+ this.handleGlobalError(err);
113
+ });
114
+ };
115
+ this.init = () => {
116
+ const global = globalThis;
117
+ if (global[ERROR_HANDLER_INSTALLED]) {
118
+ return;
119
+ }
120
+ this._listenForError();
121
+ global[ERROR_HANDLER_INSTALLED] = 1;
122
+ };
123
+ }
124
+ }
125
+
126
+ const NOOP_LOGGER = {
127
+ log() {
128
+ },
129
+ debug() {
130
+ },
131
+ info() {
132
+ },
133
+ warn() {
134
+ },
135
+ };
136
+ class LoggerService {
137
+ constructor() {
138
+ this._commonLogger = NOOP_LOGGER;
139
+ this.log = async (topic, ...args) => {
140
+ await this._commonLogger.log(topic, ...args);
141
+ };
142
+ this.debug = async (topic, ...args) => {
143
+ await this._commonLogger.debug(topic, ...args);
144
+ };
145
+ this.info = async (topic, ...args) => {
146
+ await this._commonLogger.info(topic, ...args);
147
+ };
148
+ this.warn = async (topic, ...args) => {
149
+ await this._commonLogger.warn(topic, ...args);
150
+ };
151
+ this.setLogger = (logger) => {
152
+ this._commonLogger = logger;
153
+ };
154
+ }
155
+ }
156
+
157
+ const { init, inject, provide } = createActivator("cli");
158
+
159
+ const apiServices$1 = {
160
+ telegramApiService: Symbol('telegramApiService'),
161
+ quickchartApiService: Symbol('quickchartApiService'),
162
+ };
163
+ const baseServices$1 = {
164
+ errorService: Symbol('errorService'),
165
+ loggerService: Symbol('loggerService'),
166
+ resolveService: Symbol('resolveService'),
167
+ };
168
+ const connectionServices$1 = {
169
+ moduleConnectionService: Symbol('moduleConnectionService'),
170
+ };
171
+ const mainServices$1 = {
172
+ backtestMainService: Symbol('backtestMainService'),
173
+ paperMainService: Symbol('paperMainService'),
174
+ liveMainService: Symbol('liveMainService'),
175
+ };
176
+ const logicServices$1 = {
177
+ cacheLogicService: Symbol('cacheLogicService'),
178
+ telegramLogicService: Symbol('telegramLogicService'),
179
+ };
180
+ const schemaServices$1 = {
181
+ exchangeSchemaService: Symbol('exchangeSchemaService'),
182
+ symbolSchemaService: Symbol('symbolSchemaService'),
183
+ frameSchemaService: Symbol('frameSchemaService'),
184
+ };
185
+ const providerServices$1 = {
186
+ frontendProviderService: Symbol('frontendProviderService'),
187
+ telegramProviderService: Symbol('telegramProviderService'),
188
+ liveProviderService: Symbol('liveProviderService'),
189
+ };
190
+ const webServices$1 = {
191
+ telegramWebService: Symbol('telegramWebService'),
192
+ };
193
+ const templateServices$1 = {
194
+ telegramTemplateService: Symbol('telegramTemplateService'),
195
+ };
196
+ const TYPES = {
197
+ ...apiServices$1,
198
+ ...baseServices$1,
199
+ ...connectionServices$1,
200
+ ...mainServices$1,
201
+ ...logicServices$1,
202
+ ...schemaServices$1,
203
+ ...providerServices$1,
204
+ ...webServices$1,
205
+ ...templateServices$1,
206
+ };
207
+
208
+ const entrySubject = new BehaviorSubject();
209
+
210
+ const __filename = fileURLToPath(import.meta.url);
211
+ const __dirname = path.dirname(__filename);
212
+ let _is_launched = false;
213
+ class ResolveService {
214
+ constructor() {
215
+ this.loggerService = inject(TYPES.loggerService);
216
+ this.DEFAULT_TEMPLATE_DIR = path.resolve(__dirname, '..', 'template');
217
+ this.OVERRIDE_TEMPLATE_DIR = path.resolve(process.cwd(), 'template');
218
+ this.OVERRIDE_MODULES_DIR = path.resolve(process.cwd(), 'modules');
219
+ this.attachEntryPoint = async (entryPoint) => {
220
+ this.loggerService.log("resolveService attachEntryPoint");
221
+ if (_is_launched) {
222
+ throw new Error("Entry point is already attached. Multiple entry points are not allowed.");
223
+ }
224
+ const absolutePath = path.resolve(entryPoint);
225
+ await access(absolutePath, constants.F_OK | constants.R_OK);
226
+ const moduleRoot = path.dirname(absolutePath);
227
+ {
228
+ const cwd = process.cwd();
229
+ process.chdir(moduleRoot);
230
+ dotenv.config({ path: path.join(cwd, '.env') });
231
+ dotenv.config({ path: path.join(moduleRoot, '.env'), override: true });
232
+ await import(pathToFileURL(absolutePath).href);
233
+ await entrySubject.next(absolutePath);
234
+ }
235
+ _is_launched = true;
236
+ };
237
+ }
238
+ }
239
+
240
+ const getExchange = singleshot(async () => {
241
+ const exchange = new ccxt.binance({
242
+ options: {
243
+ defaultType: "spot",
244
+ adjustForTimeDifference: true,
245
+ recvWindow: 60000,
246
+ },
247
+ enableRateLimit: true,
248
+ });
249
+ await exchange.loadMarkets();
250
+ return exchange;
251
+ });
252
+
253
+ var ExchangeName;
254
+ (function (ExchangeName) {
255
+ ExchangeName["DefaultExchange"] = "default_exchange";
256
+ })(ExchangeName || (ExchangeName = {}));
257
+ var ExchangeName$1 = ExchangeName;
258
+
259
+ const ADD_EXCHANGE_FN = (self) => {
260
+ self.loggerService.log("Adding CCXT Binance as a default exchange schema");
261
+ console.warn("Warning: The default exchange schema is set to CCXT Binance. Please make sure to update it according to your needs using --exchange cli param.");
262
+ addExchangeSchema({
263
+ exchangeName: ExchangeName$1.DefaultExchange,
264
+ getCandles: async (symbol, interval, since, limit) => {
265
+ const exchange = await getExchange();
266
+ const candles = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
267
+ return candles.map(([timestamp, open, high, low, close, volume], idx) => ({
268
+ timestamp,
269
+ open,
270
+ high,
271
+ low,
272
+ close,
273
+ volume,
274
+ }));
275
+ },
276
+ formatPrice: async (symbol, price) => {
277
+ const exchange = await getExchange();
278
+ const market = exchange.market(symbol);
279
+ const tickSize = market.limits?.price?.min || market.precision?.price;
280
+ if (tickSize !== undefined) {
281
+ return roundTicks(price, tickSize);
282
+ }
283
+ return exchange.priceToPrecision(symbol, price);
284
+ },
285
+ formatQuantity: async (symbol, quantity) => {
286
+ const exchange = await getExchange();
287
+ const market = exchange.market(symbol);
288
+ const stepSize = market.limits?.amount?.min || market.precision?.amount;
289
+ if (stepSize !== undefined) {
290
+ return roundTicks(quantity, stepSize);
291
+ }
292
+ return exchange.amountToPrecision(symbol, quantity);
293
+ },
294
+ getOrderBook: async (symbol, depth, from, to, backtest) => {
295
+ if (backtest) {
296
+ throw new Error("Order book fetching is not supported in backtest mode for the default exchange schema. Please implement it according to your needs.");
297
+ }
298
+ const exchange = await getExchange();
299
+ const bookData = await exchange.fetchOrderBook(symbol, depth);
300
+ return {
301
+ symbol,
302
+ asks: bookData.asks.map(([price, quantity]) => ({
303
+ price: String(price),
304
+ quantity: String(quantity),
305
+ })),
306
+ bids: bookData.bids.map(([price, quantity]) => ({
307
+ price: String(price),
308
+ quantity: String(quantity),
309
+ })),
310
+ };
311
+ },
312
+ });
313
+ };
314
+ class ExchangeSchemaService {
315
+ constructor() {
316
+ this.loggerService = inject(TYPES.loggerService);
317
+ this.init = singleshot(async () => {
318
+ this.loggerService.log("exchangeSchemaService init");
319
+ const { length } = await listExchangeSchema();
320
+ !length && ADD_EXCHANGE_FN(this);
321
+ });
322
+ }
323
+ }
324
+
325
+ var FrameName;
326
+ (function (FrameName) {
327
+ FrameName["DefaultFrame"] = "default_frame";
328
+ })(FrameName || (FrameName = {}));
329
+ var FrameName$1 = FrameName;
330
+
331
+ const getArgs = singleshot(() => {
332
+ const { values, positionals } = parseArgs({
333
+ args: process.argv,
334
+ options: {
335
+ symbol: {
336
+ type: "string",
337
+ default: "",
338
+ },
339
+ strategy: {
340
+ type: "string",
341
+ default: "",
342
+ },
343
+ exchange: {
344
+ type: "string",
345
+ default: "",
346
+ },
347
+ frame: {
348
+ type: "string",
349
+ default: "",
350
+ },
351
+ backtest: {
352
+ type: "boolean",
353
+ default: false,
354
+ },
355
+ live: {
356
+ type: "boolean",
357
+ default: false,
358
+ },
359
+ paper: {
360
+ type: "boolean",
361
+ default: false,
362
+ },
363
+ ui: {
364
+ type: "boolean",
365
+ default: false,
366
+ },
367
+ telegram: {
368
+ type: "boolean",
369
+ default: false,
370
+ },
371
+ verbose: {
372
+ type: "boolean",
373
+ default: false,
374
+ },
375
+ cache: {
376
+ type: "string",
377
+ default: "1m, 15m, 30m, 4h",
378
+ },
379
+ },
380
+ strict: false,
381
+ allowPositionals: true,
382
+ });
383
+ return {
384
+ values,
385
+ positionals,
386
+ };
387
+ });
388
+
389
+ const ADD_FRAME_FN = (self) => {
390
+ self.loggerService.log("Adding February 2024 as a default frame schema");
391
+ console.warn("Warning: The default frame schema is set to February 2024. Please make sure to update it according to your needs using --frame cli param.");
392
+ addFrameSchema({
393
+ frameName: FrameName$1.DefaultFrame,
394
+ interval: "1m",
395
+ startDate: new Date("2024-02-01T00:00:00Z"),
396
+ endDate: new Date("2024-02-29T23:59:59Z"),
397
+ });
398
+ };
399
+ class FrameSchemaService {
400
+ constructor() {
401
+ this.loggerService = inject(TYPES.loggerService);
402
+ this.init = singleshot(async () => {
403
+ this.loggerService.log("frameSchemaService init");
404
+ if (!getArgs().values.backtest) {
405
+ return;
406
+ }
407
+ const { length } = await listFrameSchema();
408
+ !length && ADD_FRAME_FN(this);
409
+ });
410
+ }
411
+ }
412
+
413
+ class SymbolSchemaService {
414
+ constructor() {
415
+ this.loggerService = inject(TYPES.loggerService);
416
+ this.init = singleshot(async () => {
417
+ this.loggerService.log("symbolSchemaService init");
418
+ if (!getArgs().values.symbol) {
419
+ console.warn("Warning: The default symbol is set to BTCUSDT. Please make sure to update it according to your needs using --symbol cli param.");
420
+ }
421
+ });
422
+ }
423
+ }
424
+
425
+ const notifyFinish = singleshot(() => {
426
+ let disposeRef;
427
+ const unLive = listenDoneLive(() => {
428
+ console.log("Live trading finished");
429
+ disposeRef && disposeRef();
430
+ });
431
+ const unBacktest = listenDoneBacktest(() => {
432
+ console.log("Backtest trading finished");
433
+ disposeRef && disposeRef();
434
+ });
435
+ disposeRef = compose(() => unLive(), () => unBacktest());
436
+ });
437
+
438
+ class BacktestMainService {
439
+ constructor() {
440
+ this.loggerService = inject(TYPES.loggerService);
441
+ this.exchangeSchemaService = inject(TYPES.exchangeSchemaService);
442
+ this.frameSchemaService = inject(TYPES.frameSchemaService);
443
+ this.cacheLogicService = inject(TYPES.cacheLogicService);
444
+ this.resolveService = inject(TYPES.resolveService);
445
+ this.frontendProviderService = inject(TYPES.frontendProviderService);
446
+ this.telegramProviderService = inject(TYPES.telegramProviderService);
447
+ this.init = singleshot(async () => {
448
+ this.loggerService.log("backtestMainService init");
449
+ {
450
+ this.frontendProviderService.init();
451
+ this.telegramProviderService.init();
452
+ }
453
+ const { values, positionals } = getArgs();
454
+ if (!values.backtest) {
455
+ return;
456
+ }
457
+ const [entryPoint = null] = positionals;
458
+ if (!entryPoint) {
459
+ throw new Error("Entry point is required");
460
+ }
461
+ await this.resolveService.attachEntryPoint(entryPoint);
462
+ {
463
+ this.exchangeSchemaService.init();
464
+ this.frameSchemaService.init();
465
+ }
466
+ const symbol = values.symbol || "BTCUSDT";
467
+ const [defaultStrategyName = null] = await listStrategySchema();
468
+ const [defaultExchangeName = null] = await listExchangeSchema();
469
+ const [defaultFrameName = null] = await listFrameSchema();
470
+ const strategyName = values.strategy || defaultStrategyName?.strategyName;
471
+ if (!strategyName) {
472
+ throw new Error("Strategy name is required");
473
+ }
474
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
475
+ if (!exchangeName) {
476
+ throw new Error("Exchange name is required");
477
+ }
478
+ const frameName = values.frame || defaultFrameName?.frameName;
479
+ if (!frameName) {
480
+ throw new Error("Frame name is required");
481
+ }
482
+ await this.cacheLogicService.execute({
483
+ exchangeName,
484
+ frameName,
485
+ symbol,
486
+ });
487
+ if (values.verbose) {
488
+ overrideExchangeSchema({
489
+ exchangeName,
490
+ callbacks: {
491
+ onCandleData(symbol, interval, since) {
492
+ console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
493
+ },
494
+ },
495
+ });
496
+ }
497
+ Backtest.background(symbol, {
498
+ strategyName,
499
+ frameName,
500
+ exchangeName,
501
+ });
502
+ notifyFinish();
503
+ });
504
+ }
505
+ }
506
+
507
+ class LiveMainService {
508
+ constructor() {
509
+ this.loggerService = inject(TYPES.loggerService);
510
+ this.exchangeSchemaService = inject(TYPES.exchangeSchemaService);
511
+ this.resolveService = inject(TYPES.resolveService);
512
+ this.frontendProviderService = inject(TYPES.frontendProviderService);
513
+ this.telegramProviderService = inject(TYPES.telegramProviderService);
514
+ this.init = singleshot(async () => {
515
+ this.loggerService.log("liveMainService init");
516
+ {
517
+ this.frontendProviderService.init();
518
+ this.telegramProviderService.init();
519
+ }
520
+ const { values, positionals } = getArgs();
521
+ if (!values.live) {
522
+ return;
523
+ }
524
+ const [entryPoint = null] = positionals;
525
+ if (!entryPoint) {
526
+ throw new Error("Entry point is required");
527
+ }
528
+ await this.resolveService.attachEntryPoint(entryPoint);
529
+ {
530
+ this.exchangeSchemaService.init();
531
+ }
532
+ const symbol = values.symbol || "BTCUSDT";
533
+ const [defaultStrategyName = null] = await listStrategySchema();
534
+ const [defaultExchangeName = null] = await listExchangeSchema();
535
+ const strategyName = values.strategy || defaultStrategyName?.strategyName;
536
+ if (!strategyName) {
537
+ throw new Error("Strategy name is required");
538
+ }
539
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
540
+ if (!exchangeName) {
541
+ throw new Error("Exchange name is required");
542
+ }
543
+ if (values.verbose) {
544
+ overrideExchangeSchema({
545
+ exchangeName,
546
+ callbacks: {
547
+ onCandleData(symbol, interval, since) {
548
+ console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
549
+ },
550
+ },
551
+ });
552
+ }
553
+ Live.background(symbol, {
554
+ strategyName,
555
+ exchangeName,
556
+ });
557
+ notifyFinish();
558
+ });
559
+ }
560
+ }
561
+
562
+ class PaperMainService {
563
+ constructor() {
564
+ this.loggerService = inject(TYPES.loggerService);
565
+ this.exchangeSchemaService = inject(TYPES.exchangeSchemaService);
566
+ this.resolveService = inject(TYPES.resolveService);
567
+ this.frontendProviderService = inject(TYPES.frontendProviderService);
568
+ this.telegramProviderService = inject(TYPES.telegramProviderService);
569
+ this.init = singleshot(async () => {
570
+ this.loggerService.log("paperMainService init");
571
+ {
572
+ this.frontendProviderService.init();
573
+ this.telegramProviderService.init();
574
+ }
575
+ const { values, positionals } = getArgs();
576
+ if (!values.paper) {
577
+ return;
578
+ }
579
+ const [entryPoint = null] = positionals;
580
+ if (!entryPoint) {
581
+ throw new Error("Entry point is required");
582
+ }
583
+ await this.resolveService.attachEntryPoint(entryPoint);
584
+ {
585
+ this.exchangeSchemaService.init();
586
+ }
587
+ const symbol = values.symbol || "BTCUSDT";
588
+ const [defaultStrategyName = null] = await listStrategySchema();
589
+ const [defaultExchangeName = null] = await listExchangeSchema();
590
+ const strategyName = values.strategy || defaultStrategyName?.strategyName;
591
+ if (!strategyName) {
592
+ throw new Error("Strategy name is required");
593
+ }
594
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
595
+ if (!exchangeName) {
596
+ throw new Error("Exchange name is required");
597
+ }
598
+ if (values.verbose) {
599
+ overrideExchangeSchema({
600
+ exchangeName,
601
+ callbacks: {
602
+ onCandleData(symbol, interval, since) {
603
+ console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
604
+ },
605
+ },
606
+ });
607
+ }
608
+ Live.background(symbol, {
609
+ strategyName,
610
+ exchangeName,
611
+ });
612
+ notifyFinish();
613
+ });
614
+ }
615
+ }
616
+
617
+ const getEnv = singleshot(() => {
618
+ if (!entrySubject.data) {
619
+ getEnv.clear();
620
+ }
621
+ return {
622
+ CC_TELEGRAM_TOKEN: process.env.CC_TELEGRAM_TOKEN || "",
623
+ CC_TELEGRAM_CHANNEL: process.env.CC_TELEGRAM_CHANNEL || "",
624
+ CC_WWWROOT_HOST: process.env.CC_WWWROOT_HOST || "0.0.0.0",
625
+ CC_WWWROOT_PORT: parseInt(process.env.CC_WWWROOT_PORT) || 60050,
626
+ CC_QUICKCHART_HOST: process.env.CC_QUICKCHART_HOST || "",
627
+ };
628
+ });
629
+
630
+ class FrontendProviderService {
631
+ constructor() {
632
+ this.loggerService = inject(TYPES.loggerService);
633
+ this.enable = singleshot(() => {
634
+ this.loggerService.log("frontendProviderService enable");
635
+ const { CC_WWWROOT_HOST, CC_WWWROOT_PORT } = getEnv();
636
+ const unServer = serve(CC_WWWROOT_HOST, CC_WWWROOT_PORT);
637
+ return () => {
638
+ unServer();
639
+ this.enable.clear();
640
+ };
641
+ });
642
+ this.disable = () => {
643
+ this.loggerService.log("frontendProviderService disable");
644
+ if (this.enable.hasValue()) {
645
+ const lastSubscription = this.enable();
646
+ lastSubscription();
647
+ }
648
+ };
649
+ this.init = singleshot(async () => {
650
+ this.loggerService.log("frontendProviderService init");
651
+ if (!getArgs().values.ui) {
652
+ return;
653
+ }
654
+ entrySubject.subscribe(this.enable);
655
+ });
656
+ }
657
+ }
658
+
659
+ class TelegramProviderService {
660
+ constructor() {
661
+ this.loggerService = inject(TYPES.loggerService);
662
+ this.telegramLogicService = inject(TYPES.telegramLogicService);
663
+ this.enable = singleshot(() => {
664
+ this.loggerService.log("telegramProviderService enable");
665
+ const { CC_TELEGRAM_CHANNEL, CC_TELEGRAM_TOKEN } = getEnv();
666
+ if (!CC_TELEGRAM_CHANNEL) {
667
+ console.log("CC_TELEGRAM_CHANNEL is not set, telegram provider disabled");
668
+ this.enable.clear();
669
+ return;
670
+ }
671
+ if (!CC_TELEGRAM_TOKEN) {
672
+ console.log("CC_TELEGRAM_TOKEN is not set, telegram provider disabled");
673
+ this.enable.clear();
674
+ return;
675
+ }
676
+ return this.telegramLogicService.connect();
677
+ });
678
+ this.disable = () => {
679
+ this.loggerService.log("telegramProviderService disable");
680
+ if (this.enable.hasValue()) {
681
+ const lastSubscription = this.enable();
682
+ lastSubscription();
683
+ }
684
+ };
685
+ this.init = singleshot(async () => {
686
+ this.loggerService.log("telegramProviderService init");
687
+ if (!getArgs().values.telegram) {
688
+ return;
689
+ }
690
+ entrySubject.subscribe(this.enable);
691
+ });
692
+ }
693
+ }
694
+
695
+ const CANDLES_LIMIT = 160;
696
+ const GET_CONFIG_FN = async (symbol, interval) => {
697
+ const candles = await getCandles(symbol, interval, CANDLES_LIMIT);
698
+ const labels = candles.map(({ timestamp }) => new Date(timestamp).toLocaleTimeString("en-US", {
699
+ hour: "2-digit",
700
+ minute: "2-digit",
701
+ }));
702
+ const closes = candles.map(({ close }) => close);
703
+ const chartConfig = {
704
+ type: "line",
705
+ data: {
706
+ labels,
707
+ datasets: [
708
+ {
709
+ label: `${symbol} ${interval}`,
710
+ data: closes,
711
+ borderColor: "rgb(75, 192, 192)",
712
+ borderWidth: 2,
713
+ pointRadius: 0,
714
+ fill: false,
715
+ tension: 0.2,
716
+ },
717
+ ],
718
+ },
719
+ options: {
720
+ plugins: {
721
+ title: {
722
+ display: true,
723
+ text: `${symbol} Closing Prices (${interval})`,
724
+ },
725
+ legend: { display: false },
726
+ },
727
+ scales: {
728
+ x: { display: false },
729
+ y: { display: true },
730
+ },
731
+ },
732
+ };
733
+ return chartConfig;
734
+ };
735
+ class QuickchartApiService {
736
+ constructor() {
737
+ this.loggerService = inject(TYPES.loggerService);
738
+ this.getChart = async (symbol, interval) => {
739
+ this.loggerService.log("quickchartApiService getChart", {
740
+ symbol,
741
+ });
742
+ const chartConfig = await GET_CONFIG_FN(symbol, interval);
743
+ const { CC_QUICKCHART_HOST } = getEnv();
744
+ const qc = new QuickChart();
745
+ if (CC_QUICKCHART_HOST) {
746
+ qc.setHost(CC_QUICKCHART_HOST);
747
+ }
748
+ {
749
+ qc.setConfig(chartConfig);
750
+ qc.setWidth(512);
751
+ qc.setHeight(512);
752
+ qc.setFormat("png");
753
+ }
754
+ return await qc.toBinary();
755
+ };
756
+ }
757
+ }
758
+
759
+ const HEALTH_CHECK_DELAY = 60000;
760
+ let _is_stopped = false;
761
+ const handleHealthCheck = singleshot(async (bot) => {
762
+ const fn = async () => {
763
+ if (_is_stopped) {
764
+ return;
765
+ }
766
+ try {
767
+ await bot.telegram.getMe();
768
+ setTimeout(fn, HEALTH_CHECK_DELAY);
769
+ }
770
+ catch (error) {
771
+ console.log("Bot is offline");
772
+ throw new Error("Telegram goes offline");
773
+ }
774
+ };
775
+ await fn();
776
+ });
777
+ const getTelegram = singleshot(async () => {
778
+ if (_is_stopped) {
779
+ throw new Error("Telegram provider is stopped. Restart the process to enable it again.");
780
+ }
781
+ const { CC_TELEGRAM_TOKEN } = await getEnv();
782
+ if (!CC_TELEGRAM_TOKEN) {
783
+ throw new Error("Telegram token is not set. Please set CC_TELEGRAM_TOKEN environment variable.");
784
+ }
785
+ const bot = new Telegraf(CC_TELEGRAM_TOKEN, {
786
+ handlerTimeout: Number.POSITIVE_INFINITY,
787
+ });
788
+ console.log("Bot launching");
789
+ bot.launch({
790
+ allowedUpdates: ["message", "callback_query"],
791
+ dropPendingUpdates: true,
792
+ });
793
+ await handleHealthCheck(bot);
794
+ console.log("Bot launched");
795
+ const sendMessage = async (chatId, html) => {
796
+ const { message_id } = await bot.telegram.sendMessage(chatId, html, {
797
+ parse_mode: "HTML",
798
+ disable_web_page_preview: true,
799
+ });
800
+ return message_id;
801
+ };
802
+ const removeMessage = async (chatId, messageId) => {
803
+ await bot.telegram.deleteMessage(chatId, messageId);
804
+ };
805
+ const sendDocument = async (chatId, document, filename, caption) => {
806
+ const { message_id } = await bot.telegram.sendDocument(chatId, {
807
+ source: document,
808
+ filename,
809
+ }, {
810
+ caption,
811
+ parse_mode: caption ? "HTML" : undefined,
812
+ });
813
+ return message_id;
814
+ };
815
+ const stopBot = singleshot(() => {
816
+ bot.stop();
817
+ _is_stopped = true;
818
+ });
819
+ return { bot, sendMessage, removeMessage, sendDocument, stopBot };
820
+ });
821
+
822
+ const MAX_CAPTION_SYMBOLS = 1024;
823
+ const MAX_IMAGE_WIDTH = 1000;
824
+ const MAX_IMAGE_HEIGHT = 1000;
825
+ const MAX_IMAGE_COUNT = 10;
826
+ const countAllTagsExceptBr = (html) => {
827
+ if (typeof html !== "string") {
828
+ return 0;
829
+ }
830
+ html = html.replace(/<(?!\/?br\s*\/?)[^>]+>/gi, "");
831
+ html = html.split("</br>").join("");
832
+ html = html.split("<br>").join("\n");
833
+ return html.length;
834
+ };
835
+ const PROTECT_CONTENT = false;
836
+ const PREVENT_FLOOD = 30000;
837
+ const FLOOD_MAX_RETRY = 5;
838
+ const MAX_MSG_SYMBOLS = 4096;
839
+ const MAX_TIMEOUT_COUNT = 30;
840
+ const fetchImage = async (media) => {
841
+ const request = await fetch(media);
842
+ const arrayBuffer = await request.arrayBuffer();
843
+ return Buffer.from(arrayBuffer);
844
+ };
845
+ const processImageBuffer = async (nodeBuffer) => {
846
+ const size = imageSize(nodeBuffer);
847
+ if (size.width > size.height && size.width > MAX_IMAGE_WIDTH) {
848
+ nodeBuffer = await resizeImg(nodeBuffer, { width: MAX_IMAGE_WIDTH });
849
+ }
850
+ if (size.height > size.width && size.height > MAX_IMAGE_HEIGHT) {
851
+ nodeBuffer = await resizeImg(nodeBuffer, { height: MAX_IMAGE_HEIGHT });
852
+ }
853
+ const stream = Readable.from(nodeBuffer);
854
+ return Input.fromReadableStream(stream, `${randomString()}.png`);
855
+ };
856
+ const getImageFromUrl = execpool(async (url) => {
857
+ console.log(`Image ${url} fetch begin`);
858
+ const nodeBuffer = await fetchImage(url);
859
+ console.log(`Image ${url} fetch end`);
860
+ return processImageBuffer(nodeBuffer);
861
+ }, { maxExec: 3 });
862
+ const processMedia = async (media) => {
863
+ if (typeof media === 'string') {
864
+ return getImageFromUrl(media);
865
+ }
866
+ return media;
867
+ };
868
+ const publishInternal = queued(async ({ channel, msg, images = [], onScheduled, }) => {
869
+ const { bot } = await getTelegram();
870
+ console.log("Bot sending");
871
+ onScheduled();
872
+ let isOk = true;
873
+ try {
874
+ const execute = async (retry = 0) => {
875
+ let isImagesPublished = false;
876
+ try {
877
+ if (images?.length) {
878
+ console.log("Bot fetching images");
879
+ const withCaption = countAllTagsExceptBr(msg) < MAX_CAPTION_SYMBOLS;
880
+ console.log(`Bot publishing media group withCaption=${withCaption}`);
881
+ const imageList = await Promise.all(images.slice(0, MAX_IMAGE_COUNT).map(async (media, idx) => ({
882
+ type: "photo",
883
+ media: await processMedia(media),
884
+ ...(idx === 0 &&
885
+ withCaption && { caption: msg, parse_mode: "HTML" }),
886
+ disable_web_page_preview: true,
887
+ })));
888
+ if (!isImagesPublished) {
889
+ await bot.telegram.sendMediaGroup(channel, imageList, {
890
+ protect_content: PROTECT_CONTENT,
891
+ });
892
+ isImagesPublished = true;
893
+ }
894
+ if (!withCaption) {
895
+ console.log("Bot publishing caption");
896
+ await bot.telegram.sendMessage(channel, msg, {
897
+ protect_content: PROTECT_CONTENT,
898
+ parse_mode: "HTML",
899
+ disable_web_page_preview: true,
900
+ });
901
+ }
902
+ }
903
+ else {
904
+ console.log("Bot publishing message");
905
+ await bot.telegram.sendMessage(channel, msg, {
906
+ protect_content: PROTECT_CONTENT,
907
+ parse_mode: "HTML",
908
+ disable_web_page_preview: true,
909
+ });
910
+ }
911
+ }
912
+ catch (error) {
913
+ if (retry >= FLOOD_MAX_RETRY) {
914
+ console.log(`Telegram flood max retry reached retry=${retry}`);
915
+ throw error;
916
+ }
917
+ const retry_after = error?.response?.parameters?.retry_after;
918
+ if (retry_after) {
919
+ console.log(`Telegram flood retry=${retry} retry_after=${retry_after}`);
920
+ await sleep(retry_after * 1000 + 1000);
921
+ await execute(retry + 1);
922
+ }
923
+ else {
924
+ throw error;
925
+ }
926
+ }
927
+ };
928
+ if (countAllTagsExceptBr(msg) >= MAX_MSG_SYMBOLS) {
929
+ console.log("Box max msg length reached");
930
+ console.log(msg);
931
+ console.log(msg.length);
932
+ throw new Error("Box max msg length reached");
933
+ }
934
+ await execute();
935
+ }
936
+ catch (error) {
937
+ console.error(error);
938
+ isOk = false;
939
+ }
940
+ finally {
941
+ await sleep(PREVENT_FLOOD);
942
+ if (isOk) {
943
+ console.log("Bot sent ok");
944
+ }
945
+ else {
946
+ console.log("Bot sent error");
947
+ }
948
+ }
949
+ });
950
+ let TIMEOUT_COUNTER = 0;
951
+ class TelegramApiService {
952
+ constructor() {
953
+ this.publish = async (channel, msg, images) => {
954
+ const [waitForResult, { resolve }] = createAwaiter();
955
+ const task = publishInternal({
956
+ onScheduled: () => resolve(),
957
+ channel,
958
+ msg,
959
+ images,
960
+ })
961
+ .catch((error) => {
962
+ console.error("Telegram publish failure", {
963
+ error,
964
+ });
965
+ setTimeout(() => process.exit(-1), 5000);
966
+ });
967
+ const result = await Promise.race([
968
+ waitForResult,
969
+ sleep(5000).then(() => TIMEOUT_SYMBOL),
970
+ ]);
971
+ if (result === TIMEOUT_SYMBOL) {
972
+ TIMEOUT_COUNTER += 1;
973
+ task.finally(() => {
974
+ TIMEOUT_COUNTER -= 1;
975
+ });
976
+ return "Message scheduled for publication";
977
+ }
978
+ if (TIMEOUT_COUNTER > MAX_TIMEOUT_COUNT) {
979
+ setTimeout(() => process.exit(-1), 5000);
980
+ }
981
+ return "Message published successfully";
982
+ };
983
+ }
984
+ }
985
+
986
+ const TELEGAM_MAX_SYMBOLS = 4090;
987
+ const TELEGRAM_SYMBOL_RESERVED = 96;
988
+ // Function to convert Markdown to Telegram-compatible HTML
989
+ function toTelegramHtml(markdown) {
990
+ const maxChars = TELEGAM_MAX_SYMBOLS - TELEGRAM_SYMBOL_RESERVED;
991
+ // Initialize markdown-it with options
992
+ const md = new MarkdownIt({
993
+ html: false,
994
+ breaks: true,
995
+ linkify: true,
996
+ typographer: true,
997
+ });
998
+ // Prevent removing of \u00a0
999
+ markdown = markdown.replaceAll(typo.nbsp, "&nbsp;");
1000
+ markdown = markdown.replaceAll(typo.bullet, "&bull;");
1001
+ // Render Markdown to HTML
1002
+ let telegramHtml = md.render(markdown);
1003
+ // Post-process with sanitize-html to ensure Telegram compatibility
1004
+ telegramHtml = sanitizeHtml(telegramHtml, {
1005
+ allowedTags: [
1006
+ "b",
1007
+ "i",
1008
+ "a",
1009
+ "code",
1010
+ "pre",
1011
+ "s",
1012
+ "u",
1013
+ "tg-spoiler",
1014
+ "blockquote",
1015
+ "br",
1016
+ ],
1017
+ allowedAttributes: {
1018
+ a: ["href"], // Allow href for links
1019
+ },
1020
+ transformTags: {
1021
+ // Transform headings to bold text
1022
+ h1: "b",
1023
+ h2: "b",
1024
+ h3: "b",
1025
+ h4: "b",
1026
+ h5: "b",
1027
+ h6: "b",
1028
+ // Transform strong to b
1029
+ strong: "b",
1030
+ // Transform em to i
1031
+ em: "i",
1032
+ // Remove p tags, replace with newlines
1033
+ p: () => "",
1034
+ // Emulate unordered lists with bullets
1035
+ ul: () => "",
1036
+ li: () => "- ",
1037
+ // Emulate ordered lists with numbers
1038
+ ol: () => "",
1039
+ // Remove hr, replace with text-based separator
1040
+ hr: () => "\n",
1041
+ // Remove br, replace with text-based separator
1042
+ br: () => "\n",
1043
+ // Remove divs
1044
+ div: () => "",
1045
+ },
1046
+ });
1047
+ {
1048
+ telegramHtml = telegramHtml.replaceAll(typo.bullet, "#").trim();
1049
+ }
1050
+ {
1051
+ telegramHtml = telegramHtml.replaceAll(typo.nbsp, "<br>\n").trim();
1052
+ telegramHtml = telegramHtml.replaceAll(/\n[\s\n]*\n/g, "\n").trim();
1053
+ telegramHtml = telegramHtml.replaceAll("<br>", "").trim();
1054
+ }
1055
+ // Check Telegram message length limit (4096 characters)
1056
+ if (telegramHtml.length > maxChars) {
1057
+ console.warn("HTML exceeds Telegram's 4096-character limit. Truncating...");
1058
+ telegramHtml = telegramHtml.substring(0, maxChars);
1059
+ }
1060
+ const telegramDom = new JSDOM(telegramHtml, {
1061
+ contentType: "text/html",
1062
+ resources: "usable",
1063
+ runScripts: "outside-only",
1064
+ pretendToBeVisual: false,
1065
+ });
1066
+ const document = telegramDom.window.document;
1067
+ const fragment = document.createDocumentFragment();
1068
+ const body = document.body;
1069
+ while (body.firstChild) {
1070
+ fragment.appendChild(body.firstChild);
1071
+ }
1072
+ const tempDiv = document.createElement("div");
1073
+ tempDiv.appendChild(fragment);
1074
+ return tempDiv.innerHTML;
1075
+ }
1076
+
1077
+ class TelegramWebService {
1078
+ constructor() {
1079
+ this.loggerService = inject(TYPES.loggerService);
1080
+ this.telegramApiService = inject(TYPES.telegramApiService);
1081
+ this.quickchartApiService = inject(TYPES.quickchartApiService);
1082
+ this.publishNotify = async (dto) => {
1083
+ this.loggerService.log("telegramWebService publishNotify", {
1084
+ dto,
1085
+ });
1086
+ const { CC_TELEGRAM_TOKEN, CC_TELEGRAM_CHANNEL } = getEnv();
1087
+ if (!CC_TELEGRAM_TOKEN || !CC_TELEGRAM_CHANNEL) {
1088
+ return;
1089
+ }
1090
+ const html = toTelegramHtml(dto.markdown);
1091
+ try {
1092
+ const images = await Promise.all([
1093
+ this.quickchartApiService.getChart(dto.symbol, "1m"),
1094
+ this.quickchartApiService.getChart(dto.symbol, "15m"),
1095
+ this.quickchartApiService.getChart(dto.symbol, "1h"),
1096
+ ]);
1097
+ await this.telegramApiService.publish(CC_TELEGRAM_CHANNEL, html, images.map((imageBuffer) => {
1098
+ const stream = Readable.from(imageBuffer);
1099
+ return Input.fromReadableStream(stream, `${randomString()}.png`);
1100
+ }));
1101
+ }
1102
+ catch (error) {
1103
+ this.loggerService.log(`telegramWebService publishConfirmNotify Error publishing ${dto.symbol} confirm notify: ${getErrorMessage(error)}`, {
1104
+ error: errorData(error),
1105
+ });
1106
+ await this.telegramApiService.publish(CC_TELEGRAM_CHANNEL, html);
1107
+ }
1108
+ };
1109
+ }
1110
+ }
1111
+
1112
+ const DEFAULT_TIMEFRAME_LIST = ["1m", "15m", "30m", "1h", "4h"];
1113
+ const GET_TIMEFRAME_LIST_FN = async () => {
1114
+ const { values } = getArgs();
1115
+ if (!values.cache) {
1116
+ console.warn(`Warning: No cache timeframes provided. Using default timeframes: ${DEFAULT_TIMEFRAME_LIST.join(", ")}`);
1117
+ return DEFAULT_TIMEFRAME_LIST;
1118
+ }
1119
+ return String(values.cache)
1120
+ .split(",")
1121
+ .map((timeframe) => timeframe.trim());
1122
+ };
1123
+ const GET_TIMEFRAME_RANGE_FN = async (frameName) => {
1124
+ const frameList = await listFrameSchema();
1125
+ const frameSchema = frameList.find((frameSchema) => frameSchema.frameName === frameName);
1126
+ if (!frameSchema) {
1127
+ throw new Error(`Frame with name ${frameName} not found`);
1128
+ }
1129
+ const { startDate, endDate } = frameSchema;
1130
+ return { startDate, endDate };
1131
+ };
1132
+ const CACHE_CANDLES_FN = retry(async (interval, dto) => {
1133
+ try {
1134
+ console.log(`Checking candles cache for ${dto.symbol} ${interval} from ${dto.from} to ${dto.to}`);
1135
+ await checkCandles({
1136
+ exchangeName: dto.exchangeName,
1137
+ from: dto.from,
1138
+ to: dto.to,
1139
+ symbol: dto.symbol,
1140
+ interval: interval,
1141
+ });
1142
+ }
1143
+ catch (error) {
1144
+ console.log(`Caching candles for ${dto.symbol} ${interval} from ${dto.from} to ${dto.to}`);
1145
+ await warmCandles({
1146
+ symbol: dto.symbol,
1147
+ exchangeName: dto.exchangeName,
1148
+ from: dto.from,
1149
+ to: dto.to,
1150
+ interval: interval,
1151
+ });
1152
+ throw error;
1153
+ }
1154
+ }, 2);
1155
+ class CacheLogicService {
1156
+ constructor() {
1157
+ this.loggerService = inject(TYPES.loggerService);
1158
+ this.execute = async (dto) => {
1159
+ this.loggerService.log("cacheLogicService execute", {
1160
+ dto,
1161
+ });
1162
+ const { startDate, endDate } = await GET_TIMEFRAME_RANGE_FN(dto.frameName);
1163
+ const intervalList = await GET_TIMEFRAME_LIST_FN();
1164
+ try {
1165
+ for (const interval of intervalList) {
1166
+ await CACHE_CANDLES_FN(interval, {
1167
+ symbol: dto.symbol,
1168
+ exchangeName: dto.exchangeName,
1169
+ from: startDate,
1170
+ to: endDate,
1171
+ });
1172
+ }
1173
+ }
1174
+ catch (error) {
1175
+ console.log(getErrorMessage(error));
1176
+ throw error;
1177
+ }
1178
+ };
1179
+ }
1180
+ }
1181
+
1182
+ const STOP_BOT_FN = singleshot(async () => {
1183
+ const { stopBot } = await getTelegram();
1184
+ stopBot();
1185
+ });
1186
+ class TelegramLogicService {
1187
+ constructor() {
1188
+ this.loggerService = inject(TYPES.loggerService);
1189
+ this.telegramTemplateService = inject(TYPES.telegramTemplateService);
1190
+ this.telegramWebService = inject(TYPES.telegramWebService);
1191
+ this.notifyTrailingTake = async (event) => {
1192
+ this.loggerService.log("telegramLogicService notifyTrailingTake", {
1193
+ event,
1194
+ });
1195
+ const markdown = await this.telegramTemplateService.getTrailingTakeMarkdown(event);
1196
+ await this.telegramWebService.publishNotify({
1197
+ symbol: event.symbol,
1198
+ markdown,
1199
+ });
1200
+ };
1201
+ this.notifyTrailingStop = async (event) => {
1202
+ this.loggerService.log("telegramLogicService notifyTrailingStop", {
1203
+ event,
1204
+ });
1205
+ const markdown = await this.telegramTemplateService.getTrailingStopMarkdown(event);
1206
+ await this.telegramWebService.publishNotify({
1207
+ symbol: event.symbol,
1208
+ markdown,
1209
+ });
1210
+ };
1211
+ this.notifyBreakeven = async (event) => {
1212
+ this.loggerService.log("telegramLogicService notifyBreakeven", {
1213
+ event,
1214
+ });
1215
+ const markdown = await this.telegramTemplateService.getBreakevenMarkdown(event);
1216
+ await this.telegramWebService.publishNotify({
1217
+ symbol: event.symbol,
1218
+ markdown,
1219
+ });
1220
+ };
1221
+ this.notifyPartialProfit = async (event) => {
1222
+ this.loggerService.log("telegramLogicService notifyPartialProfit", {
1223
+ event,
1224
+ });
1225
+ const markdown = await this.telegramTemplateService.getPartialProfitMarkdown(event);
1226
+ await this.telegramWebService.publishNotify({
1227
+ symbol: event.symbol,
1228
+ markdown,
1229
+ });
1230
+ };
1231
+ this.notifyPartialLoss = async (event) => {
1232
+ this.loggerService.log("telegramLogicService notifyPartialLoss", {
1233
+ event,
1234
+ });
1235
+ const markdown = await this.telegramTemplateService.getPartialLossMarkdown(event);
1236
+ await this.telegramWebService.publishNotify({
1237
+ symbol: event.symbol,
1238
+ markdown,
1239
+ });
1240
+ };
1241
+ this.notifyScheduled = async (event) => {
1242
+ this.loggerService.log("telegramLogicService notifyScheduled", {
1243
+ event,
1244
+ });
1245
+ const markdown = await this.telegramTemplateService.getScheduledMarkdown(event);
1246
+ await this.telegramWebService.publishNotify({
1247
+ symbol: event.symbol,
1248
+ markdown,
1249
+ });
1250
+ };
1251
+ this.notifyCancelled = async (event) => {
1252
+ this.loggerService.log("telegramLogicService notifyCancelled", {
1253
+ event,
1254
+ });
1255
+ const markdown = await this.telegramTemplateService.getCancelledMarkdown(event);
1256
+ await this.telegramWebService.publishNotify({
1257
+ symbol: event.symbol,
1258
+ markdown,
1259
+ });
1260
+ };
1261
+ this.notifyOpened = async (event) => {
1262
+ this.loggerService.log("telegramLogicService notifyOpened", {
1263
+ event,
1264
+ });
1265
+ const markdown = await this.telegramTemplateService.getOpenedMarkdown(event);
1266
+ await this.telegramWebService.publishNotify({
1267
+ symbol: event.symbol,
1268
+ markdown,
1269
+ });
1270
+ };
1271
+ this.notifyClosed = async (event) => {
1272
+ this.loggerService.log("telegramLogicService notifyClosed", {
1273
+ event,
1274
+ });
1275
+ const markdown = await this.telegramTemplateService.getClosedMarkdown(event);
1276
+ await this.telegramWebService.publishNotify({
1277
+ symbol: event.symbol,
1278
+ markdown,
1279
+ });
1280
+ };
1281
+ this.notifyRisk = async (event) => {
1282
+ this.loggerService.log("telegramLogicService notifyClosed", {
1283
+ event,
1284
+ });
1285
+ const markdown = await this.telegramTemplateService.getRiskMarkdown(event);
1286
+ await this.telegramWebService.publishNotify({
1287
+ symbol: event.symbol,
1288
+ markdown,
1289
+ });
1290
+ };
1291
+ this.connect = singleshot(() => {
1292
+ this.loggerService.log("telegramLogicService connect");
1293
+ const unRisk = listenRisk(async (event) => {
1294
+ await this.notifyRisk(event);
1295
+ });
1296
+ const unSignal = listenSignal(async (event) => {
1297
+ if (event.action === "scheduled") {
1298
+ await this.notifyScheduled(event);
1299
+ return;
1300
+ }
1301
+ if (event.action === "cancelled") {
1302
+ await this.notifyCancelled(event);
1303
+ return;
1304
+ }
1305
+ if (event.action === "opened") {
1306
+ await this.notifyOpened(event);
1307
+ return;
1308
+ }
1309
+ if (event.action === "closed") {
1310
+ await this.notifyClosed(event);
1311
+ return;
1312
+ }
1313
+ });
1314
+ const unCommit = listenStrategyCommit(async (event) => {
1315
+ if (event.action === "trailing-take") {
1316
+ await this.notifyTrailingTake(event);
1317
+ return;
1318
+ }
1319
+ if (event.action === "trailing-stop") {
1320
+ await this.notifyTrailingStop(event);
1321
+ return;
1322
+ }
1323
+ if (event.action === "breakeven") {
1324
+ await this.notifyBreakeven(event);
1325
+ return;
1326
+ }
1327
+ if (event.action === "partial-profit") {
1328
+ await this.notifyPartialProfit(event);
1329
+ return;
1330
+ }
1331
+ if (event.action === "partial-loss") {
1332
+ await this.notifyPartialLoss(event);
1333
+ return;
1334
+ }
1335
+ });
1336
+ const unListen = compose(() => unRisk(), () => unSignal(), () => unCommit());
1337
+ return () => {
1338
+ STOP_BOT_FN();
1339
+ unListen();
1340
+ };
1341
+ });
1342
+ }
1343
+ }
1344
+
1345
+ const READ_TEMPLATE_FN = memoize(([fileName]) => `${fileName}`, async (fileName, self) => {
1346
+ const overridePath = path.join(self.resolveService.OVERRIDE_TEMPLATE_DIR, fileName);
1347
+ const hasOverride = await fs$1
1348
+ .access(overridePath, constants.F_OK | constants.R_OK)
1349
+ .then(() => true)
1350
+ .catch(() => false);
1351
+ if (hasOverride) {
1352
+ return await fs$1.readFile(overridePath, "utf-8");
1353
+ }
1354
+ const defaultPath = path.join(self.resolveService.DEFAULT_TEMPLATE_DIR, fileName);
1355
+ return await fs$1.readFile(defaultPath, "utf-8");
1356
+ });
1357
+ const RENDER_TEMPLATE_FN = async (fileName, event, self) => {
1358
+ const template = await READ_TEMPLATE_FN(fileName, self);
1359
+ return Mustache.render(template, event);
1360
+ };
1361
+ class TelegramTemplateService {
1362
+ constructor() {
1363
+ this.loggerService = inject(TYPES.loggerService);
1364
+ this.resolveService = inject(TYPES.resolveService);
1365
+ this.getTrailingTakeMarkdown = async (event) => {
1366
+ this.loggerService.log("telegramTemplateService getTrailingTakeMarkdown", {
1367
+ event,
1368
+ });
1369
+ return await RENDER_TEMPLATE_FN("trailing-take.mustache", event, this);
1370
+ };
1371
+ this.getTrailingStopMarkdown = async (event) => {
1372
+ this.loggerService.log("telegramTemplateService getTrailingStopMarkdown", {
1373
+ event,
1374
+ });
1375
+ return await RENDER_TEMPLATE_FN("trailing-stop.mustache", event, this);
1376
+ };
1377
+ this.getBreakevenMarkdown = async (event) => {
1378
+ this.loggerService.log("telegramTemplateService getBreakevenMarkdown", {
1379
+ event,
1380
+ });
1381
+ return await RENDER_TEMPLATE_FN("breakeven.mustache", event, this);
1382
+ };
1383
+ this.getPartialProfitMarkdown = async (event) => {
1384
+ this.loggerService.log("telegramTemplateService getPartialProfitMarkdown", {
1385
+ event,
1386
+ });
1387
+ return await RENDER_TEMPLATE_FN("partial-profit.mustache", event, this);
1388
+ };
1389
+ this.getPartialLossMarkdown = async (event) => {
1390
+ this.loggerService.log("telegramTemplateService getPartialLossMarkdown", {
1391
+ event,
1392
+ });
1393
+ return await RENDER_TEMPLATE_FN("partial-loss.mustache", event, this);
1394
+ };
1395
+ this.getScheduledMarkdown = async (event) => {
1396
+ this.loggerService.log("telegramTemplateService getScheduledMarkdown", {
1397
+ event,
1398
+ });
1399
+ return await RENDER_TEMPLATE_FN("scheduled.mustache", event, this);
1400
+ };
1401
+ this.getCancelledMarkdown = async (event) => {
1402
+ this.loggerService.log("telegramTemplateService getCancelledMarkdown", {
1403
+ event,
1404
+ });
1405
+ return await RENDER_TEMPLATE_FN("cancelled.mustache", event, this);
1406
+ };
1407
+ this.getOpenedMarkdown = async (event) => {
1408
+ this.loggerService.log("telegramTemplateService getOpenedMarkdown", {
1409
+ event,
1410
+ });
1411
+ return await RENDER_TEMPLATE_FN("opened.mustache", event, this);
1412
+ };
1413
+ this.getClosedMarkdown = async (event) => {
1414
+ this.loggerService.log("telegramTemplateService getClosedMarkdown", {
1415
+ event,
1416
+ });
1417
+ return await RENDER_TEMPLATE_FN("closed.mustache", event, this);
1418
+ };
1419
+ this.getRiskMarkdown = async (event) => {
1420
+ this.loggerService.log("telegramTemplateService getRiskMarkdown", {
1421
+ event,
1422
+ });
1423
+ return await RENDER_TEMPLATE_FN("risk.mustache", event, this);
1424
+ };
1425
+ }
1426
+ }
1427
+
1428
+ const require = createRequire(import.meta.url);
1429
+ const getExtVariants = (fileName) => {
1430
+ const ext = path.extname(fileName);
1431
+ const base = ext ? fileName.slice(0, -ext.length) : fileName;
1432
+ return [fileName, `${base}.cjs`, `${base}.mjs`];
1433
+ };
1434
+ const REQUIRE_MODULE_FACTORY = (fileName) => {
1435
+ for (const variant of getExtVariants(fileName)) {
1436
+ try {
1437
+ return require(variant);
1438
+ }
1439
+ catch {
1440
+ continue;
1441
+ }
1442
+ }
1443
+ return null;
1444
+ };
1445
+ const IMPORT_MODULE_FACTORY = async (fileName) => {
1446
+ for (const variant of getExtVariants(fileName)) {
1447
+ try {
1448
+ return await import(variant);
1449
+ }
1450
+ catch {
1451
+ continue;
1452
+ }
1453
+ }
1454
+ return null;
1455
+ };
1456
+ const LOAD_MODULE_MODULE_FN = async (fileName, self) => {
1457
+ let Ctor = null;
1458
+ const overridePath = path.join(self.resolveService.OVERRIDE_MODULES_DIR, fileName);
1459
+ const targetPath = path.join(process.cwd(), "modules", fileName);
1460
+ const hasOverride = await fs$1
1461
+ .access(overridePath, constants.F_OK | constants.R_OK)
1462
+ .then(() => true)
1463
+ .catch(() => false);
1464
+ const resolvedFile = hasOverride ? overridePath : targetPath;
1465
+ if ((Ctor = REQUIRE_MODULE_FACTORY(resolvedFile))) {
1466
+ return typeof Ctor === "function" ? new Ctor() : Ctor;
1467
+ }
1468
+ if ((Ctor = await IMPORT_MODULE_FACTORY(resolvedFile))) {
1469
+ return typeof Ctor === "function" ? new Ctor() : Ctor;
1470
+ }
1471
+ throw new Error(`Module module import failed for file: ${resolvedFile}`);
1472
+ };
1473
+ class ModuleConnectionService {
1474
+ constructor() {
1475
+ this.loggerService = inject(TYPES.loggerService);
1476
+ this.resolveService = inject(TYPES.resolveService);
1477
+ this.getInstance = memoize(([fileName]) => `${fileName}`, async (fileName) => {
1478
+ this.loggerService.log("moduleConnectionService getInstance", {
1479
+ fileName,
1480
+ });
1481
+ return await LOAD_MODULE_MODULE_FN(fileName, this);
1482
+ });
1483
+ }
1484
+ }
1485
+
1486
+ const LOAD_INSTANCE_FN = singleshot(async (self) => {
1487
+ const module = (await self.moduleConnectionService.getInstance("./live.module"));
1488
+ return module;
1489
+ });
1490
+ class LiveProviderService {
1491
+ constructor() {
1492
+ this.loggerService = inject(TYPES.loggerService);
1493
+ this.moduleConnectionService = inject(TYPES.moduleConnectionService);
1494
+ this.handleTrailingTake = async (event) => {
1495
+ this.loggerService.log("liveProviderService handleTrailingTake", {
1496
+ event,
1497
+ });
1498
+ const instance = await LOAD_INSTANCE_FN(this);
1499
+ if (instance.onTrailingTake) {
1500
+ await instance.onTrailingTake(event);
1501
+ }
1502
+ };
1503
+ this.handleTrailingStop = async (event) => {
1504
+ this.loggerService.log("liveProviderService handleTrailingStop", {
1505
+ event,
1506
+ });
1507
+ const instance = await LOAD_INSTANCE_FN(this);
1508
+ if (instance.onTrailingStop) {
1509
+ await instance.onTrailingStop(event);
1510
+ }
1511
+ };
1512
+ this.handleBreakeven = async (event) => {
1513
+ this.loggerService.log("liveProviderService handleBreakeven", {
1514
+ event,
1515
+ });
1516
+ const instance = await LOAD_INSTANCE_FN(this);
1517
+ if (instance.onBreakeven) {
1518
+ await instance.onBreakeven(event);
1519
+ }
1520
+ };
1521
+ this.handlePartialProfit = async (event) => {
1522
+ this.loggerService.log("liveProviderService handlePartialProfit", {
1523
+ event,
1524
+ });
1525
+ const instance = await LOAD_INSTANCE_FN(this);
1526
+ if (instance.onPartialProfit) {
1527
+ await instance.onPartialProfit(event);
1528
+ }
1529
+ };
1530
+ this.handlePartialLoss = async (event) => {
1531
+ this.loggerService.log("liveProviderService handlePartialLoss", {
1532
+ event,
1533
+ });
1534
+ const instance = await LOAD_INSTANCE_FN(this);
1535
+ if (instance.onPartialLoss) {
1536
+ await instance.onPartialLoss(event);
1537
+ }
1538
+ };
1539
+ this.handleScheduled = async (event) => {
1540
+ this.loggerService.log("liveProviderService handleScheduled", {
1541
+ event,
1542
+ });
1543
+ const instance = await LOAD_INSTANCE_FN(this);
1544
+ if (instance.onScheduled) {
1545
+ await instance.onScheduled(event);
1546
+ }
1547
+ };
1548
+ this.handleCancelled = async (event) => {
1549
+ this.loggerService.log("liveProviderService handleCancelled", {
1550
+ event,
1551
+ });
1552
+ const instance = await LOAD_INSTANCE_FN(this);
1553
+ if (instance.onCancelled) {
1554
+ await instance.onCancelled(event);
1555
+ }
1556
+ };
1557
+ this.handleOpened = async (event) => {
1558
+ this.loggerService.log("liveProviderService handleOpened", {
1559
+ event,
1560
+ });
1561
+ const instance = await LOAD_INSTANCE_FN(this);
1562
+ if (instance.onOpened) {
1563
+ await instance.onOpened(event);
1564
+ }
1565
+ };
1566
+ this.handleClosed = async (event) => {
1567
+ this.loggerService.log("liveProviderService handleClosed", {
1568
+ event,
1569
+ });
1570
+ const instance = await LOAD_INSTANCE_FN(this);
1571
+ if (instance.onClosed) {
1572
+ await instance.onClosed(event);
1573
+ }
1574
+ };
1575
+ this.handleRisk = async (event) => {
1576
+ this.loggerService.log("liveProviderService handleClosed", {
1577
+ event,
1578
+ });
1579
+ const instance = await LOAD_INSTANCE_FN(this);
1580
+ if (instance.onRisk) {
1581
+ await instance.onRisk(event);
1582
+ }
1583
+ };
1584
+ this.enable = singleshot(() => {
1585
+ this.loggerService.log("liveProviderService enable");
1586
+ const unRisk = listenRisk(async (event) => {
1587
+ await this.handleRisk(event);
1588
+ });
1589
+ const unSignal = listenSignal(async (event) => {
1590
+ if (event.action === "scheduled") {
1591
+ await this.handleScheduled(event);
1592
+ return;
1593
+ }
1594
+ if (event.action === "cancelled") {
1595
+ await this.handleCancelled(event);
1596
+ return;
1597
+ }
1598
+ if (event.action === "opened") {
1599
+ await this.handleOpened(event);
1600
+ return;
1601
+ }
1602
+ if (event.action === "closed") {
1603
+ await this.handleClosed(event);
1604
+ return;
1605
+ }
1606
+ });
1607
+ const unCommit = listenStrategyCommit(async (event) => {
1608
+ if (event.action === "trailing-take") {
1609
+ await this.handleTrailingTake(event);
1610
+ return;
1611
+ }
1612
+ if (event.action === "trailing-stop") {
1613
+ await this.handleTrailingStop(event);
1614
+ return;
1615
+ }
1616
+ if (event.action === "breakeven") {
1617
+ await this.handleBreakeven(event);
1618
+ return;
1619
+ }
1620
+ if (event.action === "partial-profit") {
1621
+ await this.handlePartialProfit(event);
1622
+ return;
1623
+ }
1624
+ if (event.action === "partial-loss") {
1625
+ await this.handlePartialLoss(event);
1626
+ return;
1627
+ }
1628
+ });
1629
+ return compose(() => unRisk(), () => unSignal(), () => unCommit());
1630
+ });
1631
+ this.disable = () => {
1632
+ this.loggerService.log("liveProviderService disable");
1633
+ if (this.enable.hasValue()) {
1634
+ const lastSubscription = this.enable();
1635
+ lastSubscription();
1636
+ }
1637
+ };
1638
+ this.init = singleshot(async () => {
1639
+ this.loggerService.log("liveProviderService init");
1640
+ if (!getArgs().values.live) {
1641
+ return;
1642
+ }
1643
+ entrySubject.subscribe(this.enable);
1644
+ });
1645
+ }
1646
+ }
1647
+
1648
+ {
1649
+ provide(TYPES.quickchartApiService, () => new QuickchartApiService());
1650
+ provide(TYPES.telegramApiService, () => new TelegramApiService());
1651
+ }
1652
+ {
1653
+ provide(TYPES.errorService, () => new ErrorService());
1654
+ provide(TYPES.loggerService, () => new LoggerService());
1655
+ provide(TYPES.resolveService, () => new ResolveService());
1656
+ }
1657
+ {
1658
+ provide(TYPES.moduleConnectionService, () => new ModuleConnectionService());
1659
+ }
1660
+ {
1661
+ provide(TYPES.backtestMainService, () => new BacktestMainService());
1662
+ provide(TYPES.paperMainService, () => new PaperMainService());
1663
+ provide(TYPES.liveMainService, () => new LiveMainService());
1664
+ }
1665
+ {
1666
+ provide(TYPES.cacheLogicService, () => new CacheLogicService());
1667
+ provide(TYPES.telegramLogicService, () => new TelegramLogicService());
1668
+ }
1669
+ {
1670
+ provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
1671
+ provide(TYPES.symbolSchemaService, () => new SymbolSchemaService());
1672
+ provide(TYPES.frameSchemaService, () => new FrameSchemaService());
1673
+ }
1674
+ {
1675
+ provide(TYPES.telegramProviderService, () => new TelegramProviderService());
1676
+ provide(TYPES.frontendProviderService, () => new FrontendProviderService());
1677
+ provide(TYPES.liveProviderService, () => new LiveProviderService());
1678
+ }
1679
+ {
1680
+ provide(TYPES.telegramWebService, () => new TelegramWebService());
1681
+ }
1682
+ {
1683
+ provide(TYPES.telegramTemplateService, () => new TelegramTemplateService());
1684
+ }
1685
+
1686
+ const apiServices = {
1687
+ telegramApiService: inject(TYPES.telegramApiService),
1688
+ quickchartApiService: inject(TYPES.quickchartApiService),
1689
+ };
1690
+ const baseServices = {
1691
+ errorService: inject(TYPES.errorService),
1692
+ loggerService: inject(TYPES.loggerService),
1693
+ resolveService: inject(TYPES.resolveService),
1694
+ };
1695
+ const connectionServices = {
1696
+ moduleConnectionService: inject(TYPES.moduleConnectionService),
1697
+ };
1698
+ const mainServices = {
1699
+ backtestMainService: inject(TYPES.backtestMainService),
1700
+ paperMainService: inject(TYPES.paperMainService),
1701
+ liveMainService: inject(TYPES.liveMainService),
1702
+ };
1703
+ const logicServices = {
1704
+ cacheLogicService: inject(TYPES.cacheLogicService),
1705
+ telegramLogicService: inject(TYPES.telegramLogicService),
1706
+ };
1707
+ const schemaServices = {
1708
+ exchangeSchemaService: inject(TYPES.exchangeSchemaService),
1709
+ symbolSchemaService: inject(TYPES.symbolSchemaService),
1710
+ frameSchemaService: inject(TYPES.frameSchemaService),
1711
+ };
1712
+ const providerServices = {
1713
+ frontendProviderService: inject(TYPES.frontendProviderService),
1714
+ telegramProviderService: inject(TYPES.telegramProviderService),
1715
+ liveProviderService: inject(TYPES.liveProviderService),
1716
+ };
1717
+ const webServices = {
1718
+ telegramWebService: inject(TYPES.telegramWebService),
1719
+ };
1720
+ const templateServices = {
1721
+ telegramTemplateService: inject(TYPES.telegramTemplateService),
1722
+ };
1723
+ const cli = {
1724
+ ...apiServices,
1725
+ ...baseServices,
1726
+ ...connectionServices,
1727
+ ...mainServices,
1728
+ ...logicServices,
1729
+ ...schemaServices,
1730
+ ...providerServices,
1731
+ ...webServices,
1732
+ ...templateServices,
1733
+ };
1734
+ init();
1735
+
1736
+ const notifyShutdown = singleshot(async () => {
1737
+ console.log("Graceful shutdown initiated. Press Ctrl+C again to force quit.");
1738
+ });
1739
+
1740
+ const BEFORE_EXIT_FN$4 = singleshot(async () => {
1741
+ process.off("SIGINT", BEFORE_EXIT_FN$4);
1742
+ const [running = null] = await Backtest.list();
1743
+ if (!running) {
1744
+ return;
1745
+ }
1746
+ notifyShutdown();
1747
+ const { exchangeName, frameName, strategyName, symbol, status } = running;
1748
+ if (status === "fulfilled") {
1749
+ return;
1750
+ }
1751
+ Backtest.stop(symbol, {
1752
+ exchangeName,
1753
+ strategyName,
1754
+ frameName,
1755
+ });
1756
+ });
1757
+ const main$4 = async () => {
1758
+ const { values } = getArgs();
1759
+ if (!values.backtest) {
1760
+ return;
1761
+ }
1762
+ process.on("SIGINT", BEFORE_EXIT_FN$4);
1763
+ };
1764
+ main$4();
1765
+
1766
+ const BEFORE_EXIT_FN$3 = singleshot(async () => {
1767
+ process.off("SIGINT", BEFORE_EXIT_FN$3);
1768
+ const [running = null] = await Live.list();
1769
+ if (!running) {
1770
+ return;
1771
+ }
1772
+ notifyShutdown();
1773
+ const { exchangeName, strategyName, symbol, status } = running;
1774
+ if (status === "fulfilled") {
1775
+ return;
1776
+ }
1777
+ Live.stop(symbol, {
1778
+ exchangeName,
1779
+ strategyName,
1780
+ });
1781
+ });
1782
+ const main$3 = async () => {
1783
+ const { values } = getArgs();
1784
+ if (!values.paper) {
1785
+ return;
1786
+ }
1787
+ process.on("SIGINT", BEFORE_EXIT_FN$3);
1788
+ };
1789
+ main$3();
1790
+
1791
+ const BEFORE_EXIT_FN$2 = singleshot(async () => {
1792
+ process.off("SIGINT", BEFORE_EXIT_FN$2);
1793
+ const [running = null] = await Live.list();
1794
+ if (!running) {
1795
+ return;
1796
+ }
1797
+ notifyShutdown();
1798
+ const { exchangeName, strategyName, symbol, status } = running;
1799
+ if (status === "fulfilled") {
1800
+ return;
1801
+ }
1802
+ Live.stop(symbol, {
1803
+ exchangeName,
1804
+ strategyName,
1805
+ });
1806
+ listenDoneLive(cli.liveProviderService.disable);
1807
+ });
1808
+ const main$2 = async () => {
1809
+ const { values } = getArgs();
1810
+ if (!values.live) {
1811
+ return;
1812
+ }
1813
+ process.on("SIGINT", BEFORE_EXIT_FN$2);
1814
+ };
1815
+ main$2();
1816
+
1817
+ const BEFORE_EXIT_FN$1 = singleshot(async () => {
1818
+ process.off("SIGINT", BEFORE_EXIT_FN$1);
1819
+ notifyShutdown();
1820
+ cli.frontendProviderService.disable();
1821
+ });
1822
+ const main$1 = async () => {
1823
+ const { values } = getArgs();
1824
+ if (!values.ui) {
1825
+ return;
1826
+ }
1827
+ process.on("SIGINT", BEFORE_EXIT_FN$1);
1828
+ };
1829
+ main$1();
1830
+
1831
+ const BEFORE_EXIT_FN = singleshot(async () => {
1832
+ process.off("SIGINT", BEFORE_EXIT_FN);
1833
+ notifyShutdown();
1834
+ cli.telegramProviderService.disable();
1835
+ });
1836
+ const main = async () => {
1837
+ const { values } = getArgs();
1838
+ if (!values.telegram) {
1839
+ return;
1840
+ }
1841
+ process.on("SIGINT", BEFORE_EXIT_FN);
1842
+ };
1843
+ main();
1844
+
1845
+ function setLogger(logger) {
1846
+ cli.loggerService.setLogger(logger);
1847
+ }
1848
+
1849
+ export { cli, setLogger };