@backtest-kit/pinets 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.
- package/README.md +218 -0
- package/build/index.cjs +588 -0
- package/build/index.mjs +578 -0
- package/package.json +88 -0
- package/types.d.ts +159 -0
package/build/index.cjs
ADDED
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var path = require('path');
|
|
4
|
+
var backtestKit = require('backtest-kit');
|
|
5
|
+
var diKit = require('di-kit');
|
|
6
|
+
var functoolsKit = require('functools-kit');
|
|
7
|
+
var fs = require('fs/promises');
|
|
8
|
+
var module$1 = require('module');
|
|
9
|
+
|
|
10
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
11
|
+
const CODE_TYPE_SYMBOL = Symbol("code-type");
|
|
12
|
+
class Code {
|
|
13
|
+
constructor(source) {
|
|
14
|
+
this.source = source;
|
|
15
|
+
this.__type__ = CODE_TYPE_SYMBOL;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
Code.fromString = (source) => {
|
|
19
|
+
if (!source || typeof source !== "string" || !source.trim()) {
|
|
20
|
+
throw new Error("Source must be a non-empty string");
|
|
21
|
+
}
|
|
22
|
+
return new Code(source);
|
|
23
|
+
};
|
|
24
|
+
Code.isCode = (value) => {
|
|
25
|
+
return (value !== null &&
|
|
26
|
+
typeof value === "object" &&
|
|
27
|
+
value.__type__ === CODE_TYPE_SYMBOL);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const FILE_TYPE_SYMBOL = Symbol("file-type");
|
|
31
|
+
class File {
|
|
32
|
+
constructor(path, baseDir) {
|
|
33
|
+
this.path = path;
|
|
34
|
+
this.baseDir = baseDir;
|
|
35
|
+
this.__type__ = FILE_TYPE_SYMBOL;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
File.fromPath = (path$1, baseDir = path.join(process.cwd(), "config/source")) => {
|
|
39
|
+
if (!path$1 || typeof path$1 !== "string" || !path$1.trim()) {
|
|
40
|
+
throw new Error("Path must be a non-empty string");
|
|
41
|
+
}
|
|
42
|
+
return new File(path$1, baseDir);
|
|
43
|
+
};
|
|
44
|
+
File.isFile = (value) => {
|
|
45
|
+
return (value !== null &&
|
|
46
|
+
typeof value === "object" &&
|
|
47
|
+
value.__type__ === FILE_TYPE_SYMBOL);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const NOOP_LOGGER = {
|
|
51
|
+
log() {
|
|
52
|
+
},
|
|
53
|
+
debug() {
|
|
54
|
+
},
|
|
55
|
+
info() {
|
|
56
|
+
},
|
|
57
|
+
warn() {
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
class LoggerService {
|
|
61
|
+
constructor() {
|
|
62
|
+
this._commonLogger = NOOP_LOGGER;
|
|
63
|
+
this.log = async (topic, ...args) => {
|
|
64
|
+
await this._commonLogger.log(topic, ...args);
|
|
65
|
+
};
|
|
66
|
+
this.debug = async (topic, ...args) => {
|
|
67
|
+
await this._commonLogger.debug(topic, ...args);
|
|
68
|
+
};
|
|
69
|
+
this.info = async (topic, ...args) => {
|
|
70
|
+
await this._commonLogger.info(topic, ...args);
|
|
71
|
+
};
|
|
72
|
+
this.warn = async (topic, ...args) => {
|
|
73
|
+
await this._commonLogger.warn(topic, ...args);
|
|
74
|
+
};
|
|
75
|
+
this.setLogger = (logger) => {
|
|
76
|
+
this._commonLogger = logger;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { provide, inject, init, override } = diKit.createActivator("pine");
|
|
82
|
+
|
|
83
|
+
const baseServices = {
|
|
84
|
+
loggerService: Symbol("loggerService"),
|
|
85
|
+
};
|
|
86
|
+
const providerServices$1 = {
|
|
87
|
+
axisProviderService: Symbol("axisProviderService"),
|
|
88
|
+
candleProviderService: Symbol("candleProviderService"),
|
|
89
|
+
};
|
|
90
|
+
const jobServices$1 = {
|
|
91
|
+
pineJobService: Symbol("pineJobService"),
|
|
92
|
+
};
|
|
93
|
+
const dataServices$1 = {
|
|
94
|
+
pineDataService: Symbol("pineDataService"),
|
|
95
|
+
};
|
|
96
|
+
const cacheServices$1 = {
|
|
97
|
+
pineCacheService: Symbol("pineCacheService"),
|
|
98
|
+
};
|
|
99
|
+
const connectionServices$1 = {
|
|
100
|
+
pineConnectionService: Symbol("pineConnectionService"),
|
|
101
|
+
};
|
|
102
|
+
const TYPES = {
|
|
103
|
+
...baseServices,
|
|
104
|
+
...providerServices$1,
|
|
105
|
+
...jobServices$1,
|
|
106
|
+
...dataServices$1,
|
|
107
|
+
...cacheServices$1,
|
|
108
|
+
...connectionServices$1,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const MS_PER_MINUTE = 60000;
|
|
112
|
+
const INTERVAL_MINUTES = {
|
|
113
|
+
"1m": 1,
|
|
114
|
+
"3m": 3,
|
|
115
|
+
"5m": 5,
|
|
116
|
+
"15m": 15,
|
|
117
|
+
"30m": 30,
|
|
118
|
+
"1h": 60,
|
|
119
|
+
"2h": 120,
|
|
120
|
+
"4h": 240,
|
|
121
|
+
"6h": 360,
|
|
122
|
+
"8h": 480,
|
|
123
|
+
};
|
|
124
|
+
const AXIS_SYMBOL = "_AXIS";
|
|
125
|
+
class AxisProviderService {
|
|
126
|
+
constructor() {
|
|
127
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
128
|
+
}
|
|
129
|
+
async getMarketData(_, timeframe, limit, sDate, eDate) {
|
|
130
|
+
this.loggerService.log("axisProviderService getMarketData", {
|
|
131
|
+
timeframe,
|
|
132
|
+
limit,
|
|
133
|
+
sDate,
|
|
134
|
+
eDate,
|
|
135
|
+
});
|
|
136
|
+
const step = INTERVAL_MINUTES[timeframe];
|
|
137
|
+
if (!step) {
|
|
138
|
+
throw new Error(`AxisProvider getMarketData: unknown timeframe=${timeframe}. Allowed values: ${Object.keys(INTERVAL_MINUTES).join(", ")}`);
|
|
139
|
+
}
|
|
140
|
+
const currentDate = await backtestKit.getDate();
|
|
141
|
+
const whenTimestamp = currentDate.getTime();
|
|
142
|
+
const intervalMs = step * MS_PER_MINUTE;
|
|
143
|
+
let sinceTimestamp;
|
|
144
|
+
let untilTimestamp;
|
|
145
|
+
let calculatedLimit;
|
|
146
|
+
if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
|
|
147
|
+
if (sDate >= eDate) {
|
|
148
|
+
throw new Error(`AxisProvider getMarketData: sDate (${sDate}) must be < eDate (${eDate})`);
|
|
149
|
+
}
|
|
150
|
+
if (eDate > whenTimestamp) {
|
|
151
|
+
throw new Error(`AxisProvider getMarketData: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
152
|
+
}
|
|
153
|
+
sinceTimestamp = sDate;
|
|
154
|
+
untilTimestamp = eDate;
|
|
155
|
+
calculatedLimit = limit;
|
|
156
|
+
}
|
|
157
|
+
else if (sDate !== undefined && eDate !== undefined && limit === undefined) {
|
|
158
|
+
if (sDate >= eDate) {
|
|
159
|
+
throw new Error(`AxisProvider getMarketData: sDate (${sDate}) must be < eDate (${eDate})`);
|
|
160
|
+
}
|
|
161
|
+
if (eDate > whenTimestamp) {
|
|
162
|
+
throw new Error(`AxisProvider getMarketData: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
163
|
+
}
|
|
164
|
+
sinceTimestamp = sDate;
|
|
165
|
+
untilTimestamp = eDate;
|
|
166
|
+
calculatedLimit = Math.ceil((eDate - sDate) / intervalMs);
|
|
167
|
+
if (calculatedLimit <= 0) {
|
|
168
|
+
throw new Error(`AxisProvider getMarketData: calculated limit is ${calculatedLimit}, must be > 0`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (sDate === undefined && eDate !== undefined && limit !== undefined) {
|
|
172
|
+
if (eDate > whenTimestamp) {
|
|
173
|
+
throw new Error(`AxisProvider getMarketData: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
174
|
+
}
|
|
175
|
+
untilTimestamp = eDate;
|
|
176
|
+
sinceTimestamp = eDate - limit * intervalMs;
|
|
177
|
+
calculatedLimit = limit;
|
|
178
|
+
}
|
|
179
|
+
else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
|
|
180
|
+
sinceTimestamp = sDate;
|
|
181
|
+
untilTimestamp = sDate + limit * intervalMs;
|
|
182
|
+
if (untilTimestamp > whenTimestamp) {
|
|
183
|
+
throw new Error(`AxisProvider getMarketData: calculated endTimestamp (${untilTimestamp}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
184
|
+
}
|
|
185
|
+
calculatedLimit = limit;
|
|
186
|
+
}
|
|
187
|
+
else if (sDate === undefined && eDate === undefined && limit !== undefined) {
|
|
188
|
+
untilTimestamp = whenTimestamp;
|
|
189
|
+
sinceTimestamp = whenTimestamp - limit * intervalMs;
|
|
190
|
+
calculatedLimit = limit;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
throw new Error(`AxisProvider getMarketData: invalid parameter combination. ` +
|
|
194
|
+
`Provide one of: (sDate+eDate+limit), (sDate+eDate), (eDate+limit), (sDate+limit), or (limit only). ` +
|
|
195
|
+
`Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
|
|
196
|
+
}
|
|
197
|
+
const candles = [];
|
|
198
|
+
for (let i = 0; i < calculatedLimit; i++) {
|
|
199
|
+
const openTime = sinceTimestamp + i * intervalMs;
|
|
200
|
+
if (openTime >= untilTimestamp) {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
candles.push({
|
|
204
|
+
openTime,
|
|
205
|
+
open: 0,
|
|
206
|
+
high: 0,
|
|
207
|
+
low: 0,
|
|
208
|
+
close: 0,
|
|
209
|
+
volume: 0,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return candles;
|
|
213
|
+
}
|
|
214
|
+
async getSymbolInfo() {
|
|
215
|
+
this.loggerService.log("axisProviderService getSymbolInfo");
|
|
216
|
+
const result = {
|
|
217
|
+
ticker: AXIS_SYMBOL,
|
|
218
|
+
tickerid: AXIS_SYMBOL,
|
|
219
|
+
description: "Time Axis",
|
|
220
|
+
type: "index",
|
|
221
|
+
basecurrency: "",
|
|
222
|
+
currency: "",
|
|
223
|
+
timezone: "UTC",
|
|
224
|
+
};
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
class CandleProviderService {
|
|
230
|
+
constructor() {
|
|
231
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
232
|
+
}
|
|
233
|
+
async getMarketData(tickerId, timeframe, limit, sDate, eDate) {
|
|
234
|
+
this.loggerService.log("candleProviderService getMarketData", {
|
|
235
|
+
tickerId,
|
|
236
|
+
timeframe,
|
|
237
|
+
limit,
|
|
238
|
+
sDate,
|
|
239
|
+
eDate,
|
|
240
|
+
});
|
|
241
|
+
const symbol = tickerId
|
|
242
|
+
.toUpperCase()
|
|
243
|
+
.replace(/^BINANCE:|^BYBIT:|^OKX:/, "");
|
|
244
|
+
const rawCandles = await backtestKit.getRawCandles(symbol, timeframe, limit, sDate, eDate);
|
|
245
|
+
const candles = rawCandles.map((c) => ({
|
|
246
|
+
openTime: c.timestamp,
|
|
247
|
+
open: c.open,
|
|
248
|
+
high: c.high,
|
|
249
|
+
low: c.low,
|
|
250
|
+
close: c.close,
|
|
251
|
+
volume: c.volume,
|
|
252
|
+
}));
|
|
253
|
+
return candles;
|
|
254
|
+
}
|
|
255
|
+
async getSymbolInfo(tickerId) {
|
|
256
|
+
this.loggerService.log("candleProviderService getSymbolInfo", {
|
|
257
|
+
tickerId,
|
|
258
|
+
});
|
|
259
|
+
const symbol = tickerId
|
|
260
|
+
.toUpperCase()
|
|
261
|
+
.replace(/^BINANCE:|^BYBIT:|^OKX:/, "");
|
|
262
|
+
const base = symbol.replace(/USDT$|BUSD$|USD$/, "");
|
|
263
|
+
const quote = symbol.replace(base, "");
|
|
264
|
+
const result = {
|
|
265
|
+
ticker: symbol,
|
|
266
|
+
tickerid: symbol,
|
|
267
|
+
description: `${base}/${quote}`,
|
|
268
|
+
type: "crypto",
|
|
269
|
+
basecurrency: base,
|
|
270
|
+
currency: quote || "USDT",
|
|
271
|
+
timezone: "UTC",
|
|
272
|
+
};
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const CREATE_PROVIDER_FN = functoolsKit.singleshot((self) => ({
|
|
278
|
+
async getMarketData(tickerId, timeframe, limit, sDate, eDate) {
|
|
279
|
+
if (tickerId === AXIS_SYMBOL) {
|
|
280
|
+
return await self.axisProviderService.getMarketData(tickerId, timeframe, limit, sDate, eDate);
|
|
281
|
+
}
|
|
282
|
+
return await self.candleProviderService.getMarketData(tickerId, timeframe, limit, sDate, eDate);
|
|
283
|
+
},
|
|
284
|
+
async getSymbolInfo(tickerId) {
|
|
285
|
+
if (tickerId === AXIS_SYMBOL) {
|
|
286
|
+
return await self.axisProviderService.getSymbolInfo();
|
|
287
|
+
}
|
|
288
|
+
return await self.candleProviderService.getSymbolInfo(tickerId);
|
|
289
|
+
},
|
|
290
|
+
}));
|
|
291
|
+
const CREATE_RUNNER_FN = async (self, tickerId, timeframe, limit) => {
|
|
292
|
+
const provider = CREATE_PROVIDER_FN(self);
|
|
293
|
+
const instance = await self.pineConnectionService.getInstance(provider, tickerId, timeframe, limit);
|
|
294
|
+
await instance.ready();
|
|
295
|
+
return instance;
|
|
296
|
+
};
|
|
297
|
+
class PineJobService {
|
|
298
|
+
constructor() {
|
|
299
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
300
|
+
this.axisProviderService = inject(TYPES.axisProviderService);
|
|
301
|
+
this.candleProviderService = inject(TYPES.candleProviderService);
|
|
302
|
+
this.pineConnectionService = inject(TYPES.pineConnectionService);
|
|
303
|
+
this.run = async (code, tickerId, timeframe = "1m", limit = 100) => {
|
|
304
|
+
this.loggerService.log("pineJobService run", {
|
|
305
|
+
script: code.source,
|
|
306
|
+
tickerId,
|
|
307
|
+
timeframe,
|
|
308
|
+
limit,
|
|
309
|
+
});
|
|
310
|
+
const runner = await CREATE_RUNNER_FN(this, tickerId, timeframe, limit);
|
|
311
|
+
return await runner.run(code.source);
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const GET_VALUE_FN = (plots, name, barsBack = 0) => {
|
|
317
|
+
const data = plots[name]?.data;
|
|
318
|
+
if (!data || data.length === 0)
|
|
319
|
+
return 0;
|
|
320
|
+
const idx = data.length - 1 - barsBack;
|
|
321
|
+
return idx >= 0 ? (data[idx]?.value ?? 0) : 0;
|
|
322
|
+
};
|
|
323
|
+
class PineDataService {
|
|
324
|
+
constructor() {
|
|
325
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
326
|
+
}
|
|
327
|
+
extract(plots, mapping) {
|
|
328
|
+
this.loggerService.log("pineDataService extract", {
|
|
329
|
+
plotCount: Object.keys(plots).length,
|
|
330
|
+
mapping,
|
|
331
|
+
});
|
|
332
|
+
const result = {};
|
|
333
|
+
for (const key in mapping) {
|
|
334
|
+
const config = mapping[key];
|
|
335
|
+
if (typeof config === "string") {
|
|
336
|
+
Object.assign(result, { [key]: GET_VALUE_FN(plots, config) });
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
const value = GET_VALUE_FN(plots, config.plot, config.barsBack ?? 0);
|
|
340
|
+
Object.assign(result, {
|
|
341
|
+
[key]: config.transform ? config.transform(value) : value,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const READ_FILE_FN = functoolsKit.memoize(([filePath]) => filePath, async (filePath) => {
|
|
350
|
+
const fileContent = await fs.readFile(filePath, "utf-8");
|
|
351
|
+
return fileContent;
|
|
352
|
+
});
|
|
353
|
+
class PineCacheService {
|
|
354
|
+
constructor() {
|
|
355
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
356
|
+
this.readFile = async (path$1, baseDir = path.join(process.cwd(), "config/source")) => {
|
|
357
|
+
this.loggerService.log("pineCacheService readFile", {
|
|
358
|
+
path: path$1,
|
|
359
|
+
baseDir,
|
|
360
|
+
});
|
|
361
|
+
const filePath = path.join(baseDir, path$1);
|
|
362
|
+
return await READ_FILE_FN(filePath);
|
|
363
|
+
};
|
|
364
|
+
this.clear = async (path$1, baseDir = path.join(process.cwd(), "config/source")) => {
|
|
365
|
+
this.loggerService.log("pineCacheService clear", {
|
|
366
|
+
path: path$1,
|
|
367
|
+
baseDir,
|
|
368
|
+
});
|
|
369
|
+
if (path$1) {
|
|
370
|
+
const filePath = path.join(baseDir, path$1);
|
|
371
|
+
READ_FILE_FN.clear(filePath);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
READ_FILE_FN.clear();
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const require$1 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
380
|
+
const REQUIRE_PINE_FACTORY = () => {
|
|
381
|
+
try {
|
|
382
|
+
// @ts-ignore
|
|
383
|
+
const { PineTS } = require$1("pinets");
|
|
384
|
+
// @ts-ignore
|
|
385
|
+
return PineTS;
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const IMPORT_PINE_FACTORY = async () => {
|
|
392
|
+
try {
|
|
393
|
+
// @ts-ignore
|
|
394
|
+
const { PineTS } = await import('pinets');
|
|
395
|
+
// @ts-ignore
|
|
396
|
+
return PineTS;
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
const LOAD_PINE_FACTORY_FN = functoolsKit.singleshot(async () => {
|
|
403
|
+
let ctor = null;
|
|
404
|
+
if (ctor = REQUIRE_PINE_FACTORY()) {
|
|
405
|
+
return ctor;
|
|
406
|
+
}
|
|
407
|
+
if (ctor = await IMPORT_PINE_FACTORY()) {
|
|
408
|
+
return ctor;
|
|
409
|
+
}
|
|
410
|
+
throw new Error("PineTS import failed. Call usePine to provide a PineTS class to @backtest-kit/pinets.");
|
|
411
|
+
});
|
|
412
|
+
class PineConnectionService {
|
|
413
|
+
constructor() {
|
|
414
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
415
|
+
this.getInstance = async (...args) => {
|
|
416
|
+
this.loggerService.log("pineConnectionService getInstance", {
|
|
417
|
+
args,
|
|
418
|
+
});
|
|
419
|
+
if (!this.PineFactory) {
|
|
420
|
+
this.PineFactory = await LOAD_PINE_FACTORY_FN();
|
|
421
|
+
}
|
|
422
|
+
if (!this.PineFactory) {
|
|
423
|
+
throw new Error("PineTS import failed. Call usePine to provide a PineTS class to @backtest-kit/pinets.");
|
|
424
|
+
}
|
|
425
|
+
return Reflect.construct(this.PineFactory, args);
|
|
426
|
+
};
|
|
427
|
+
this.usePine = (ctor) => {
|
|
428
|
+
this.loggerService.log("pineConnectionService usePine", {
|
|
429
|
+
ctor,
|
|
430
|
+
});
|
|
431
|
+
this.PineFactory = ctor;
|
|
432
|
+
};
|
|
433
|
+
this.clear = () => {
|
|
434
|
+
this.loggerService.log("pineConnectionService clear");
|
|
435
|
+
LOAD_PINE_FACTORY_FN.clear();
|
|
436
|
+
this.PineFactory = null;
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
{
|
|
442
|
+
provide(TYPES.loggerService, () => new LoggerService());
|
|
443
|
+
}
|
|
444
|
+
{
|
|
445
|
+
provide(TYPES.axisProviderService, () => new AxisProviderService());
|
|
446
|
+
provide(TYPES.candleProviderService, () => new CandleProviderService());
|
|
447
|
+
}
|
|
448
|
+
{
|
|
449
|
+
provide(TYPES.pineJobService, () => new PineJobService());
|
|
450
|
+
}
|
|
451
|
+
{
|
|
452
|
+
provide(TYPES.pineDataService, () => new PineDataService());
|
|
453
|
+
}
|
|
454
|
+
{
|
|
455
|
+
provide(TYPES.pineCacheService, () => new PineCacheService());
|
|
456
|
+
}
|
|
457
|
+
{
|
|
458
|
+
provide(TYPES.pineConnectionService, () => new PineConnectionService());
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const commonServices = {
|
|
462
|
+
loggerService: inject(TYPES.loggerService),
|
|
463
|
+
};
|
|
464
|
+
const providerServices = {
|
|
465
|
+
axisProviderService: inject(TYPES.axisProviderService),
|
|
466
|
+
candleProviderService: inject(TYPES.candleProviderService),
|
|
467
|
+
};
|
|
468
|
+
const jobServices = {
|
|
469
|
+
pineJobService: inject(TYPES.pineJobService),
|
|
470
|
+
};
|
|
471
|
+
const dataServices = {
|
|
472
|
+
pineDataService: inject(TYPES.pineDataService),
|
|
473
|
+
};
|
|
474
|
+
const cacheServices = {
|
|
475
|
+
pineCacheService: inject(TYPES.pineCacheService),
|
|
476
|
+
};
|
|
477
|
+
const connectionServices = {
|
|
478
|
+
pineConnectionService: inject(TYPES.pineConnectionService),
|
|
479
|
+
};
|
|
480
|
+
const pine = {
|
|
481
|
+
...commonServices,
|
|
482
|
+
...providerServices,
|
|
483
|
+
...jobServices,
|
|
484
|
+
...dataServices,
|
|
485
|
+
...cacheServices,
|
|
486
|
+
...connectionServices,
|
|
487
|
+
};
|
|
488
|
+
init();
|
|
489
|
+
|
|
490
|
+
const METHOD_NAME_USE_PINE = "pine.usePine";
|
|
491
|
+
function usePine(ctor) {
|
|
492
|
+
pine.loggerService.log(METHOD_NAME_USE_PINE, {
|
|
493
|
+
ctor,
|
|
494
|
+
});
|
|
495
|
+
pine.pineConnectionService.usePine(ctor);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const METHOD_NAME_RUN$1 = "run.run";
|
|
499
|
+
const GET_SOURCE_FN$1 = async (source) => {
|
|
500
|
+
if (File.isFile(source)) {
|
|
501
|
+
const code = await pine.pineCacheService.readFile(source.path, source.baseDir);
|
|
502
|
+
return Code.fromString(code);
|
|
503
|
+
}
|
|
504
|
+
if (Code.isCode(source)) {
|
|
505
|
+
return source;
|
|
506
|
+
}
|
|
507
|
+
throw new Error("Source must be a File or Code instance");
|
|
508
|
+
};
|
|
509
|
+
async function run(source, { symbol, timeframe, mapping, limit }) {
|
|
510
|
+
pine.loggerService.info(METHOD_NAME_RUN$1, {
|
|
511
|
+
source,
|
|
512
|
+
symbol,
|
|
513
|
+
timeframe,
|
|
514
|
+
mapping,
|
|
515
|
+
limit,
|
|
516
|
+
});
|
|
517
|
+
const script = await GET_SOURCE_FN$1(source);
|
|
518
|
+
const { plots } = await pine.pineJobService.run(script, symbol, timeframe, limit);
|
|
519
|
+
return pine.pineDataService.extract(plots, mapping);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function setLogger(logger) {
|
|
523
|
+
pine.loggerService.setLogger(logger);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const METHOD_NAME_RUN = "strategy.getSignal";
|
|
527
|
+
const DEFAULT_ESTIMATED_TIME = 240;
|
|
528
|
+
const GET_SOURCE_FN = async (source) => {
|
|
529
|
+
if (File.isFile(source)) {
|
|
530
|
+
const code = await pine.pineCacheService.readFile(source.path, source.baseDir);
|
|
531
|
+
return Code.fromString(code);
|
|
532
|
+
}
|
|
533
|
+
if (Code.isCode(source)) {
|
|
534
|
+
return source;
|
|
535
|
+
}
|
|
536
|
+
throw new Error("Source must be a File or Code instance");
|
|
537
|
+
};
|
|
538
|
+
const SIGNAL_SCHEMA = {
|
|
539
|
+
position: "Signal",
|
|
540
|
+
priceOpen: "Close",
|
|
541
|
+
priceTakeProfit: "TakeProfit",
|
|
542
|
+
priceStopLoss: "StopLoss",
|
|
543
|
+
minuteEstimatedTime: {
|
|
544
|
+
plot: "EstimatedTime",
|
|
545
|
+
transform: (v) => v || DEFAULT_ESTIMATED_TIME,
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
function toSignalDto(data) {
|
|
549
|
+
if (data.position === 1) {
|
|
550
|
+
return {
|
|
551
|
+
position: "long",
|
|
552
|
+
priceOpen: data.priceOpen,
|
|
553
|
+
priceTakeProfit: data.priceTakeProfit,
|
|
554
|
+
priceStopLoss: data.priceStopLoss,
|
|
555
|
+
minuteEstimatedTime: data.minuteEstimatedTime,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
if (data.position === -1) {
|
|
559
|
+
return {
|
|
560
|
+
position: "short",
|
|
561
|
+
priceOpen: data.priceOpen,
|
|
562
|
+
priceTakeProfit: data.priceTakeProfit,
|
|
563
|
+
priceStopLoss: data.priceStopLoss,
|
|
564
|
+
minuteEstimatedTime: data.minuteEstimatedTime,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
async function getSignal(source, { symbol, timeframe, limit }) {
|
|
570
|
+
pine.loggerService.info(METHOD_NAME_RUN, {
|
|
571
|
+
source,
|
|
572
|
+
symbol,
|
|
573
|
+
timeframe,
|
|
574
|
+
limit,
|
|
575
|
+
});
|
|
576
|
+
const { plots } = await pine.pineJobService.run(await GET_SOURCE_FN(source), symbol, timeframe, limit);
|
|
577
|
+
const data = pine.pineDataService.extract(plots, SIGNAL_SCHEMA);
|
|
578
|
+
return toSignalDto(data);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
exports.AXIS_SYMBOL = AXIS_SYMBOL;
|
|
582
|
+
exports.Code = Code;
|
|
583
|
+
exports.File = File;
|
|
584
|
+
exports.getSignal = getSignal;
|
|
585
|
+
exports.lib = pine;
|
|
586
|
+
exports.run = run;
|
|
587
|
+
exports.setLogger = setLogger;
|
|
588
|
+
exports.usePine = usePine;
|