@backtest-kit/sidekick 0.1.2 → 3.0.0
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 +120 -13
- package/content/config/source/timeframe_15m.pine +114 -0
- package/content/config/source/timeframe_4h.pine +57 -0
- package/content/config/symbol.config.cjs +460 -0
- package/content/docker/ollama/docker-compose.yaml +34 -0
- package/content/docker/ollama/watch.sh +2 -0
- package/content/scripts/cache/cache_candles.mjs +47 -0
- package/content/scripts/cache/cache_model.mjs +42 -0
- package/content/scripts/cache/validate_candles.mjs +46 -0
- package/content/scripts/run_timeframe_15m.mjs +77 -0
- package/content/scripts/run_timeframe_4h.mjs +68 -0
- package/package.json +2 -2
- package/scripts/init.mjs +40 -9
- package/src/classes/BacktestLowerStopOnBreakevenAction.mjs +18 -0
- package/src/classes/BacktestPartialProfitTakingAction.mjs +5 -0
- package/src/classes/BacktestPositionMonitorAction.mjs +4 -0
- package/src/config/setup.mjs +27 -1
- package/src/enum/ActionName.mjs +1 -1
- package/src/enum/FrameName.mjs +1 -0
- package/src/enum/RiskName.mjs +1 -1
- package/src/logic/action/backtest_lower_stop_on_breakeven.action.mjs +9 -0
- package/src/logic/exchange/binance.exchange.mjs +12 -2
- package/src/logic/frame/feb_2024.frame.mjs +10 -0
- package/src/logic/index.mjs +3 -2
- package/src/logic/risk/sl_distance.risk.mjs +32 -0
- package/src/logic/risk/tp_distance.risk.mjs +5 -3
- package/src/logic/strategy/main.strategy.mjs +29 -12
- package/src/main/bootstrap.mjs +1 -1
- package/src/math/timeframe_15m.math.mjs +68 -0
- package/src/math/timeframe_4h.math.mjs +53 -0
- package/template/CLAUDE.mustache +421 -0
- package/template/README.mustache +232 -24
- package/template/env.mustache +1 -17
- package/template/jsconfig.json.mustache +1 -0
- package/template/package.mustache +8 -5
- package/src/classes/BacktestTightenStopOnBreakevenAction.mjs +0 -13
- package/src/func/market.func.mjs +0 -46
- package/src/logic/action/backtest_tighten_stop_on_breakeven.action.mjs +0 -9
- package/src/logic/risk/rr_ratio.risk.mjs +0 -39
- /package/{types/backtest-kit.d.ts → template/types.mustache} +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { addExchangeSchema } from "backtest-kit";
|
|
2
|
+
import { singleshot, randomString } from "functools-kit";
|
|
3
|
+
import { run, File, toMarkdown } from "@backtest-kit/pinets";
|
|
4
|
+
import ccxt from "ccxt";
|
|
5
|
+
|
|
6
|
+
const SIGNAL_SCHEMA = {
|
|
7
|
+
position: "Signal",
|
|
8
|
+
priceOpen: "Close",
|
|
9
|
+
priceTakeProfit: "TakeProfit",
|
|
10
|
+
priceStopLoss: "StopLoss",
|
|
11
|
+
minuteEstimatedTime: "EstimatedTime",
|
|
12
|
+
d_RSI: "d_RSI",
|
|
13
|
+
d_EmaFast: "d_EmaFast",
|
|
14
|
+
d_EmaSlow: "d_EmaSlow",
|
|
15
|
+
d_EmaTrend: "d_EmaTrend",
|
|
16
|
+
d_ATR: "d_ATR",
|
|
17
|
+
d_Volume: "d_Volume",
|
|
18
|
+
d_VolMA: "d_VolMA",
|
|
19
|
+
d_VolSpike: "d_VolSpike",
|
|
20
|
+
d_Mom: "d_Mom",
|
|
21
|
+
d_MomUp: "d_MomUp",
|
|
22
|
+
d_MomDown: "d_MomDown",
|
|
23
|
+
d_TrendUp: "d_TrendUp",
|
|
24
|
+
d_TrendDown: "d_TrendDown",
|
|
25
|
+
d_LongCond: "d_LongCond",
|
|
26
|
+
d_ShortCond: "d_ShortCond",
|
|
27
|
+
d_BarsSinceSignal: "d_BarsSinceSignal",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const SIGNAL_ID = randomString();
|
|
31
|
+
|
|
32
|
+
const getExchange = singleshot(async () => {
|
|
33
|
+
const exchange = new ccxt.binance({
|
|
34
|
+
options: {
|
|
35
|
+
defaultType: "spot",
|
|
36
|
+
adjustForTimeDifference: true,
|
|
37
|
+
recvWindow: 60000,
|
|
38
|
+
},
|
|
39
|
+
enableRateLimit: true,
|
|
40
|
+
});
|
|
41
|
+
await exchange.loadMarkets();
|
|
42
|
+
return exchange;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
addExchangeSchema({
|
|
46
|
+
exchangeName: "ccxt-exchange",
|
|
47
|
+
getCandles: async (symbol, interval, since, limit) => {
|
|
48
|
+
const exchange = await getExchange();
|
|
49
|
+
const candles = await exchange.fetchOHLCV(
|
|
50
|
+
symbol,
|
|
51
|
+
interval,
|
|
52
|
+
since.getTime(),
|
|
53
|
+
limit,
|
|
54
|
+
);
|
|
55
|
+
return candles.map(([timestamp, open, high, low, close, volume]) => ({
|
|
56
|
+
timestamp,
|
|
57
|
+
open,
|
|
58
|
+
high,
|
|
59
|
+
low,
|
|
60
|
+
close,
|
|
61
|
+
volume,
|
|
62
|
+
}));
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const plots = await run(
|
|
67
|
+
File.fromPath("timeframe_15m.pine"),
|
|
68
|
+
{
|
|
69
|
+
symbol: "BTCUSDT",
|
|
70
|
+
timeframe: "15m",
|
|
71
|
+
limit: 60,
|
|
72
|
+
},
|
|
73
|
+
"ccxt-exchange",
|
|
74
|
+
new Date("2025-09-23T16:00:00.000Z"),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
console.log(await toMarkdown(SIGNAL_ID, plots, SIGNAL_SCHEMA));
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { addExchangeSchema } from "backtest-kit";
|
|
2
|
+
import { singleshot, randomString } from "functools-kit";
|
|
3
|
+
import { run, File, toMarkdown } from "@backtest-kit/pinets";
|
|
4
|
+
import ccxt from "ccxt";
|
|
5
|
+
|
|
6
|
+
const SIGNAL_SCHEMA = {
|
|
7
|
+
allowLong: "AllowLong",
|
|
8
|
+
allowShort: "AllowShort",
|
|
9
|
+
allowBoth: "AllowBoth",
|
|
10
|
+
noTrades: "NoTrades",
|
|
11
|
+
rsi: "RSI",
|
|
12
|
+
adx: "ADX",
|
|
13
|
+
d_MACDLine: "d_MACDLine",
|
|
14
|
+
d_SignalLine: "d_SignalLine",
|
|
15
|
+
d_MACDHist: "d_MACDHist",
|
|
16
|
+
d_DIPlus: "d_DIPlus",
|
|
17
|
+
d_DIMinus: "d_DIMinus",
|
|
18
|
+
d_StrongTrend: "d_StrongTrend",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const SIGNAL_ID = randomString();
|
|
22
|
+
|
|
23
|
+
const getExchange = singleshot(async () => {
|
|
24
|
+
const exchange = new ccxt.binance({
|
|
25
|
+
options: {
|
|
26
|
+
defaultType: "spot",
|
|
27
|
+
adjustForTimeDifference: true,
|
|
28
|
+
recvWindow: 60000,
|
|
29
|
+
},
|
|
30
|
+
enableRateLimit: true,
|
|
31
|
+
});
|
|
32
|
+
await exchange.loadMarkets();
|
|
33
|
+
return exchange;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
addExchangeSchema({
|
|
37
|
+
exchangeName: "ccxt-exchange",
|
|
38
|
+
getCandles: async (symbol, interval, since, limit) => {
|
|
39
|
+
const exchange = await getExchange();
|
|
40
|
+
const candles = await exchange.fetchOHLCV(
|
|
41
|
+
symbol,
|
|
42
|
+
interval,
|
|
43
|
+
since.getTime(),
|
|
44
|
+
limit,
|
|
45
|
+
);
|
|
46
|
+
return candles.map(([timestamp, open, high, low, close, volume]) => ({
|
|
47
|
+
timestamp,
|
|
48
|
+
open,
|
|
49
|
+
high,
|
|
50
|
+
low,
|
|
51
|
+
close,
|
|
52
|
+
volume,
|
|
53
|
+
}));
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const plots = await run(
|
|
58
|
+
File.fromPath("timeframe_4h.pine"),
|
|
59
|
+
{
|
|
60
|
+
symbol: "BTCUSDT",
|
|
61
|
+
timeframe: "4h",
|
|
62
|
+
limit: 60,
|
|
63
|
+
},
|
|
64
|
+
"ccxt-exchange",
|
|
65
|
+
new Date("2025-09-23T23:00:00.000Z"),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
console.log(await toMarkdown(SIGNAL_ID, plots, SIGNAL_SCHEMA));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backtest-kit/sidekick",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "The easiest way to create a new Backtest Kit trading bot project. Like create-react-app, but for algorithmic trading with LLM integration and technical analysis.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Petr Tripolsky",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"src",
|
|
38
38
|
"scripts",
|
|
39
39
|
"template",
|
|
40
|
-
"
|
|
40
|
+
"content",
|
|
41
41
|
"README.md"
|
|
42
42
|
],
|
|
43
43
|
"repository": {
|
package/scripts/init.mjs
CHANGED
|
@@ -110,7 +110,7 @@ async function main() {
|
|
|
110
110
|
|
|
111
111
|
const projectPath = path.resolve(process.cwd(), projectName);
|
|
112
112
|
const srcTemplatePath = path.resolve(__dirname, '..', 'src');
|
|
113
|
-
const
|
|
113
|
+
const contentPath = path.resolve(__dirname, '..', 'content');
|
|
114
114
|
const templateDir = path.resolve(__dirname, '..', 'template');
|
|
115
115
|
|
|
116
116
|
// Template data for Mustache
|
|
@@ -141,16 +141,32 @@ async function main() {
|
|
|
141
141
|
|
|
142
142
|
// Copy source template files using glob
|
|
143
143
|
{
|
|
144
|
-
log.info('Copying
|
|
144
|
+
log.info('Copying source files...');
|
|
145
145
|
await copyFiles(srcTemplatePath, path.join(projectPath, 'src'));
|
|
146
|
-
log.success('Copied
|
|
146
|
+
log.success('Copied source files');
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
// Copy
|
|
149
|
+
// Copy content files (config/, scripts/, docker/)
|
|
150
150
|
{
|
|
151
|
-
log.info('Copying
|
|
152
|
-
await copyFiles(
|
|
153
|
-
log.success('Copied
|
|
151
|
+
log.info('Copying content files...');
|
|
152
|
+
await copyFiles(contentPath, projectPath);
|
|
153
|
+
log.success('Copied content files');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Create types/backtest-kit.d.ts from template
|
|
157
|
+
{
|
|
158
|
+
log.info('Creating types/backtest-kit.d.ts...');
|
|
159
|
+
await fs.mkdir(path.join(projectPath, 'types'), { recursive: true });
|
|
160
|
+
const typesContent = await renderTemplate(
|
|
161
|
+
path.join(templateDir, 'types.mustache'),
|
|
162
|
+
templateData
|
|
163
|
+
);
|
|
164
|
+
await fs.writeFile(
|
|
165
|
+
path.join(projectPath, 'types', 'backtest-kit.d.ts'),
|
|
166
|
+
typesContent,
|
|
167
|
+
'utf-8'
|
|
168
|
+
);
|
|
169
|
+
log.success('Created types/backtest-kit.d.ts');
|
|
154
170
|
}
|
|
155
171
|
|
|
156
172
|
// Create package.json from template
|
|
@@ -216,7 +232,6 @@ async function main() {
|
|
|
216
232
|
'utf-8'
|
|
217
233
|
);
|
|
218
234
|
log.success('Created README.md');
|
|
219
|
-
console.log();
|
|
220
235
|
}
|
|
221
236
|
|
|
222
237
|
// Create jsconfig.json from template
|
|
@@ -234,6 +249,22 @@ async function main() {
|
|
|
234
249
|
log.success('Created jsconfig.json');
|
|
235
250
|
}
|
|
236
251
|
|
|
252
|
+
// Create CLAUDE.md from template
|
|
253
|
+
{
|
|
254
|
+
log.info('Creating CLAUDE.md...');
|
|
255
|
+
const claudeContent = await renderTemplate(
|
|
256
|
+
path.join(templateDir, 'CLAUDE.mustache'),
|
|
257
|
+
templateData
|
|
258
|
+
);
|
|
259
|
+
await fs.writeFile(
|
|
260
|
+
path.join(projectPath, 'CLAUDE.md'),
|
|
261
|
+
claudeContent,
|
|
262
|
+
'utf-8'
|
|
263
|
+
);
|
|
264
|
+
log.success('Created CLAUDE.md');
|
|
265
|
+
console.log();
|
|
266
|
+
}
|
|
267
|
+
|
|
237
268
|
// Install dependencies
|
|
238
269
|
{
|
|
239
270
|
await runNpmInstall(projectPath);
|
|
@@ -249,7 +280,7 @@ async function main() {
|
|
|
249
280
|
console.log('Inside that directory, you can run several commands:');
|
|
250
281
|
console.log();
|
|
251
282
|
console.log(` ${pc.cyan('npm start')}`);
|
|
252
|
-
console.log('
|
|
283
|
+
console.log(' Runs the default backtest.');
|
|
253
284
|
console.log();
|
|
254
285
|
console.log('We suggest that you begin by typing:');
|
|
255
286
|
console.log();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ActionBase, commitTrailingStop } from "backtest-kit";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lowers trailing-stop by 3 points when breakeven is reached (ignores volatility)
|
|
5
|
+
* @implements {bt.IPublicAction}
|
|
6
|
+
*/
|
|
7
|
+
export class BacktestLowerStopOnBreakevenAction extends ActionBase {
|
|
8
|
+
/**
|
|
9
|
+
*
|
|
10
|
+
* @param {bt.BreakevenContract} param0
|
|
11
|
+
*/
|
|
12
|
+
async breakevenAvailable({ symbol, currentPrice }) {
|
|
13
|
+
// Lower trailing-stop by 3 points (negative value brings stop-loss closer to entry)
|
|
14
|
+
await commitTrailingStop(symbol, -3, currentPrice);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default BacktestLowerStopOnBreakevenAction;
|
|
@@ -2,8 +2,13 @@ import { ActionBase, Constant, commitPartialProfit } from "backtest-kit";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Scale out at Kelly-optimized levels
|
|
5
|
+
* @implements {bt.IPublicAction}
|
|
5
6
|
*/
|
|
6
7
|
export class BacktestPartialProfitTakingAction extends ActionBase {
|
|
8
|
+
/**
|
|
9
|
+
*
|
|
10
|
+
* @param {bt.PartialProfitContract} param0
|
|
11
|
+
*/
|
|
7
12
|
async partialProfitAvailable({ symbol, level }) {
|
|
8
13
|
if (level === Constant.TP_LEVEL3) {
|
|
9
14
|
await commitPartialProfit(symbol, 33);
|
|
@@ -2,8 +2,12 @@ import { ActionBase } from "backtest-kit";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Monitors position lifecycle and logs open/close events in backtest mode
|
|
5
|
+
* @implements {bt.IPublicAction}
|
|
5
6
|
*/
|
|
6
7
|
export class BacktestPositionMonitorAction extends ActionBase {
|
|
8
|
+
/**
|
|
9
|
+
* @param {bt.IStrategyTickResult} event
|
|
10
|
+
*/
|
|
7
11
|
async signalBacktest(event) {
|
|
8
12
|
switch (event.action) {
|
|
9
13
|
case "scheduled":
|
package/src/config/setup.mjs
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Markdown,
|
|
3
|
+
Report,
|
|
4
|
+
Notification,
|
|
5
|
+
Storage,
|
|
6
|
+
setLogger,
|
|
7
|
+
StorageLive,
|
|
8
|
+
StorageBacktest,
|
|
9
|
+
NotificationLive,
|
|
10
|
+
NotificationBacktest,
|
|
11
|
+
} from "backtest-kit";
|
|
12
|
+
import { serve } from "@backtest-kit/ui";
|
|
2
13
|
import { createLogger } from "pinolog";
|
|
3
14
|
|
|
4
15
|
{
|
|
@@ -11,9 +22,24 @@ import { createLogger } from "pinolog";
|
|
|
11
22
|
});
|
|
12
23
|
}
|
|
13
24
|
|
|
25
|
+
{
|
|
26
|
+
Storage.enable();
|
|
27
|
+
Notification.enable();
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
{
|
|
15
31
|
Markdown.disable();
|
|
16
32
|
Report.enable();
|
|
17
33
|
}
|
|
18
34
|
|
|
35
|
+
{
|
|
36
|
+
StorageLive.usePersist();
|
|
37
|
+
StorageBacktest.usePersist();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
{
|
|
41
|
+
NotificationLive.usePersist();
|
|
42
|
+
NotificationBacktest.usePersist();
|
|
43
|
+
}
|
|
19
44
|
|
|
45
|
+
serve();
|
package/src/enum/ActionName.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
BacktestPartialProfitTakingAction: "backtest_partial_profit_taking_action",
|
|
3
|
-
|
|
3
|
+
BacktestLowerStopOnBreakevenAction: "backtest_lower_stop_on_breakeven_action",
|
|
4
4
|
BacktestPositionMonitorAction: "backtest_position_monitor_action",
|
|
5
5
|
};
|
package/src/enum/FrameName.mjs
CHANGED
package/src/enum/RiskName.mjs
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { addActionSchema } from "backtest-kit";
|
|
2
|
+
import ActionName from "../../enum/ActionName.mjs";
|
|
3
|
+
import { BacktestLowerStopOnBreakevenAction } from "../../classes/BacktestLowerStopOnBreakevenAction.mjs";
|
|
4
|
+
|
|
5
|
+
addActionSchema({
|
|
6
|
+
actionName: ActionName.BacktestLowerStopOnBreakevenAction,
|
|
7
|
+
handler: BacktestLowerStopOnBreakevenAction,
|
|
8
|
+
note: "Lower trailing-stop by 3 points when breakeven is reached",
|
|
9
|
+
});
|
|
@@ -12,8 +12,13 @@ addExchangeSchema({
|
|
|
12
12
|
symbol,
|
|
13
13
|
interval,
|
|
14
14
|
since.getTime(),
|
|
15
|
-
limit
|
|
15
|
+
limit,
|
|
16
16
|
);
|
|
17
|
+
if (
|
|
18
|
+
candles.flatMap((candle) => candle).some((value) => value === undefined)
|
|
19
|
+
) {
|
|
20
|
+
throw new Error("Invalid candles found");
|
|
21
|
+
}
|
|
17
22
|
return candles.map(([timestamp, open, high, low, close, volume]) => ({
|
|
18
23
|
timestamp,
|
|
19
24
|
open,
|
|
@@ -55,5 +60,10 @@ addExchangeSchema({
|
|
|
55
60
|
quantity: String(quantity),
|
|
56
61
|
})),
|
|
57
62
|
};
|
|
58
|
-
}
|
|
63
|
+
},
|
|
64
|
+
callbacks: {
|
|
65
|
+
onCandleData(symbol, interval, since) {
|
|
66
|
+
console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
59
69
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { addFrameSchema } from "backtest-kit";
|
|
2
|
+
import FrameName from "../../enum/FrameName.mjs";
|
|
3
|
+
|
|
4
|
+
addFrameSchema({
|
|
5
|
+
frameName: FrameName.February2024,
|
|
6
|
+
interval: "1m",
|
|
7
|
+
startDate: new Date("2024-02-01T00:00:00Z"),
|
|
8
|
+
endDate: new Date("2024-02-29T23:59:59Z"),
|
|
9
|
+
note: "Bull run period",
|
|
10
|
+
});
|
package/src/logic/index.mjs
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import "./exchange/binance.exchange.mjs";
|
|
2
2
|
|
|
3
3
|
import "./action/backtest_partial_profit_taking.action.mjs";
|
|
4
|
-
import "./action/
|
|
4
|
+
import "./action/backtest_lower_stop_on_breakeven.action.mjs";
|
|
5
5
|
import "./action/backtest_position_monitor.action.mjs";
|
|
6
6
|
|
|
7
7
|
import "./frame/dec_2025.frame.mjs";
|
|
8
8
|
import "./frame/nov_2025.frame.mjs";
|
|
9
9
|
import "./frame/oct_2025.frame.mjs";
|
|
10
|
+
import "./frame/feb_2024.frame.mjs";
|
|
10
11
|
|
|
11
|
-
import "./risk/
|
|
12
|
+
import "./risk/sl_distance.risk.mjs";
|
|
12
13
|
import "./risk/tp_distance.risk.mjs";
|
|
13
14
|
|
|
14
15
|
import "./strategy/main.strategy.mjs";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { addRiskSchema } from "backtest-kit";
|
|
2
|
+
import RiskName from "../../enum/RiskName.mjs";
|
|
3
|
+
|
|
4
|
+
const SLIPPAGE_THRESHOLD = 0.2;
|
|
5
|
+
|
|
6
|
+
addRiskSchema({
|
|
7
|
+
riskName: RiskName.StopLossDistanceRisk,
|
|
8
|
+
validations: [
|
|
9
|
+
{
|
|
10
|
+
validate: ({ currentSignal, currentPrice }) => {
|
|
11
|
+
const {
|
|
12
|
+
priceOpen = currentPrice,
|
|
13
|
+
priceStopLoss,
|
|
14
|
+
position,
|
|
15
|
+
} = currentSignal;
|
|
16
|
+
if (!priceOpen) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// Calculate SL distance percentage
|
|
20
|
+
const slDistance =
|
|
21
|
+
position === "long"
|
|
22
|
+
? ((priceOpen - priceStopLoss) / priceOpen) * 100
|
|
23
|
+
: ((priceStopLoss - priceOpen) / priceOpen) * 100;
|
|
24
|
+
|
|
25
|
+
if (slDistance < SLIPPAGE_THRESHOLD) {
|
|
26
|
+
throw new Error(`SL distance ${slDistance.toFixed(2)}% < 1%`);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
note: "SL distance must be at least 1%",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
});
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { addRiskSchema } from "backtest-kit";
|
|
2
2
|
import RiskName from "../../enum/RiskName.mjs";
|
|
3
3
|
|
|
4
|
+
const SLIPPAGE_THRESHOLD = 0.2;
|
|
5
|
+
|
|
4
6
|
addRiskSchema({
|
|
5
7
|
riskName: RiskName.TakeProfitDistanceRisk,
|
|
6
8
|
validations: [
|
|
7
9
|
{
|
|
8
|
-
validate: ({
|
|
10
|
+
validate: ({ currentSignal, currentPrice }) => {
|
|
9
11
|
const {
|
|
10
12
|
priceOpen = currentPrice,
|
|
11
13
|
priceTakeProfit,
|
|
12
14
|
position,
|
|
13
|
-
} =
|
|
15
|
+
} = currentSignal;
|
|
14
16
|
if (!priceOpen) {
|
|
15
17
|
return;
|
|
16
18
|
}
|
|
@@ -20,7 +22,7 @@ addRiskSchema({
|
|
|
20
22
|
? ((priceTakeProfit - priceOpen) / priceOpen) * 100
|
|
21
23
|
: ((priceOpen - priceTakeProfit) / priceOpen) * 100;
|
|
22
24
|
|
|
23
|
-
if (tpDistance <
|
|
25
|
+
if (tpDistance < SLIPPAGE_THRESHOLD) {
|
|
24
26
|
throw new Error(`TP distance ${tpDistance.toFixed(2)}% < 1%`);
|
|
25
27
|
}
|
|
26
28
|
},
|
|
@@ -1,31 +1,48 @@
|
|
|
1
1
|
import { addStrategySchema } from "backtest-kit";
|
|
2
|
-
import {
|
|
2
|
+
import { randomString } from "functools-kit";
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import * as math_15m from "../../math/timeframe_15m.math.mjs";
|
|
5
|
+
import * as math_4h from "../../math/timeframe_4h.math.mjs";
|
|
5
6
|
|
|
6
7
|
import StrategyName from "../../enum/StrategyName.mjs";
|
|
7
8
|
import RiskName from "../../enum/RiskName.mjs";
|
|
8
9
|
|
|
9
|
-
import { CC_OLLAMA_API_KEY } from "../../config/params.mjs";
|
|
10
|
-
|
|
11
10
|
addStrategySchema({
|
|
12
11
|
strategyName: StrategyName.MainStrategy,
|
|
13
12
|
interval: "5m",
|
|
14
13
|
getSignal: async (symbol) => {
|
|
15
|
-
|
|
14
|
+
|
|
15
|
+
const signalId = randomString();
|
|
16
|
+
|
|
17
|
+
const data_4h = await math_4h.getData(signalId, symbol);
|
|
18
|
+
|
|
19
|
+
if (data_4h.noTrades) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const data_15m = await math_15m.getData(signalId, symbol);
|
|
24
|
+
|
|
25
|
+
if (data_15m.position === 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (data_4h.allowShort && data_15m.position === 1) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (data_4h.allowLong && data_15m.position === -1) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
16
36
|
|
|
17
37
|
{
|
|
18
|
-
|
|
38
|
+
math_15m.dumpPlot(signalId, symbol);
|
|
39
|
+
math_4h.dumpPlot(signalId, symbol);
|
|
19
40
|
}
|
|
20
41
|
|
|
21
|
-
return await
|
|
22
|
-
messages,
|
|
23
|
-
"glm-4.6:cloud",
|
|
24
|
-
CC_OLLAMA_API_KEY
|
|
25
|
-
);
|
|
42
|
+
return await math_15m.getSignal(signalId, symbol);
|
|
26
43
|
},
|
|
27
44
|
riskList: [
|
|
28
45
|
RiskName.TakeProfitDistanceRisk,
|
|
29
|
-
RiskName.
|
|
46
|
+
RiskName.StopLossDistanceRisk
|
|
30
47
|
],
|
|
31
48
|
});
|
package/src/main/bootstrap.mjs
CHANGED
|
@@ -11,7 +11,7 @@ const beginBacktest = async () => {
|
|
|
11
11
|
strategyName,
|
|
12
12
|
actions: [
|
|
13
13
|
ActionName.BacktestPartialProfitTakingAction,
|
|
14
|
-
ActionName.
|
|
14
|
+
ActionName.BacktestLowerStopOnBreakevenAction,
|
|
15
15
|
ActionName.BacktestPositionMonitorAction,
|
|
16
16
|
],
|
|
17
17
|
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Cache } from "backtest-kit";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
run,
|
|
5
|
+
File,
|
|
6
|
+
toMarkdown,
|
|
7
|
+
toSignalDto,
|
|
8
|
+
extract,
|
|
9
|
+
dumpPlotData,
|
|
10
|
+
} from "@backtest-kit/pinets";
|
|
11
|
+
|
|
12
|
+
const SIGNAL_SCHEMA = {
|
|
13
|
+
position: "Signal",
|
|
14
|
+
priceOpen: "Close",
|
|
15
|
+
priceTakeProfit: "TakeProfit",
|
|
16
|
+
priceStopLoss: "StopLoss",
|
|
17
|
+
minuteEstimatedTime: "EstimatedTime",
|
|
18
|
+
d_RSI: "d_RSI",
|
|
19
|
+
d_EmaFast: "d_EmaFast",
|
|
20
|
+
d_EmaSlow: "d_EmaSlow",
|
|
21
|
+
d_EmaTrend: "d_EmaTrend",
|
|
22
|
+
d_ATR: "d_ATR",
|
|
23
|
+
d_Volume: "d_Volume",
|
|
24
|
+
d_VolMA: "d_VolMA",
|
|
25
|
+
d_VolSpike: "d_VolSpike",
|
|
26
|
+
d_Mom: "d_Mom",
|
|
27
|
+
d_MomUp: "d_MomUp",
|
|
28
|
+
d_MomDown: "d_MomDown",
|
|
29
|
+
d_TrendUp: "d_TrendUp",
|
|
30
|
+
d_TrendDown: "d_TrendDown",
|
|
31
|
+
d_LongCond: "d_LongCond",
|
|
32
|
+
d_ShortCond: "d_ShortCond",
|
|
33
|
+
d_BarsSinceSignal: "d_BarsSinceSignal",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const getPlot = Cache.fn(
|
|
37
|
+
async (symbol) =>
|
|
38
|
+
await run(File.fromPath("timeframe_15m.pine"), {
|
|
39
|
+
symbol,
|
|
40
|
+
timeframe: "15m",
|
|
41
|
+
limit: 100,
|
|
42
|
+
}),
|
|
43
|
+
{
|
|
44
|
+
interval: "15m",
|
|
45
|
+
key: ([symbol]) => `${symbol}`,
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
export const getMarkdown = async (signalId, symbol) => {
|
|
50
|
+
const plots = await getPlot(symbol);
|
|
51
|
+
return await toMarkdown(signalId, plots, SIGNAL_SCHEMA);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const getData = async (signalId, symbol) => {
|
|
55
|
+
const plots = await getPlot(symbol);
|
|
56
|
+
return await extract(plots, SIGNAL_SCHEMA);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const getSignal = async (signalId, symbol) => {
|
|
60
|
+
const plots = await getPlot(symbol);
|
|
61
|
+
const result = await extract(plots, SIGNAL_SCHEMA);
|
|
62
|
+
return toSignalDto(signalId, result, null);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const dumpPlot = async (signalId, symbol) => {
|
|
66
|
+
const plots = await getPlot(symbol);
|
|
67
|
+
dumpPlotData(signalId, plots, SIGNAL_SCHEMA, "math_15m");
|
|
68
|
+
};
|