@backtest-kit/sidekick 0.1.1 → 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/{PartialProfitTakingAction.mjs → BacktestPartialProfitTakingAction.mjs} +7 -2
- package/src/classes/BacktestPositionMonitorAction.mjs +57 -0
- package/src/config/params.mjs +1 -0
- package/src/config/setup.mjs +27 -1
- package/src/enum/ActionName.mjs +3 -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/action/backtest_partial_profit_taking.action.mjs +9 -0
- package/src/logic/action/backtest_position_monitor.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 +5 -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 -10
- package/src/main/bootstrap.mjs +52 -0
- package/src/math/timeframe_15m.math.mjs +68 -0
- package/src/math/timeframe_4h.math.mjs +53 -0
- package/src/utils/getArgs.mjs +15 -23
- 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 -6
- package/src/func/market.func.mjs +0 -46
- package/src/logic/action/partial_profit_taking.action.mjs +0 -8
- 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
|
-
export class
|
|
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);
|
|
@@ -17,4 +22,4 @@ export class PartialProfitTakingAction extends ActionBase {
|
|
|
17
22
|
}
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
export default
|
|
25
|
+
export default BacktestPartialProfitTakingAction;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ActionBase } from "backtest-kit";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Monitors position lifecycle and logs open/close events in backtest mode
|
|
5
|
+
* @implements {bt.IPublicAction}
|
|
6
|
+
*/
|
|
7
|
+
export class BacktestPositionMonitorAction extends ActionBase {
|
|
8
|
+
/**
|
|
9
|
+
* @param {bt.IStrategyTickResult} event
|
|
10
|
+
*/
|
|
11
|
+
async signalBacktest(event) {
|
|
12
|
+
switch (event.action) {
|
|
13
|
+
case "scheduled":
|
|
14
|
+
console.log(`[POSITION SCHEDULED] ${event.symbol}`);
|
|
15
|
+
console.log(` Strategy: ${event.strategyName}`);
|
|
16
|
+
console.log(` Current Price: ${event.currentPrice}`);
|
|
17
|
+
console.log(` Entry Price: ${event.signal.priceOpen}`);
|
|
18
|
+
console.log(` Signal ID: ${event.signal.id}`);
|
|
19
|
+
console.log(` Direction: ${event.signal.position}`);
|
|
20
|
+
console.log(` Stop Loss: ${event.signal.priceStopLoss}`);
|
|
21
|
+
console.log(` Take Profit: ${event.signal.priceTakeProfit}`);
|
|
22
|
+
break;
|
|
23
|
+
|
|
24
|
+
case "opened":
|
|
25
|
+
console.log(`[POSITION OPENED] ${event.symbol}`);
|
|
26
|
+
console.log(` Strategy: ${event.strategyName}`);
|
|
27
|
+
console.log(` Entry Price: ${event.currentPrice}`);
|
|
28
|
+
console.log(` Signal ID: ${event.signal.id}`);
|
|
29
|
+
console.log(` Direction: ${event.signal.position}`);
|
|
30
|
+
console.log(` Stop Loss: ${event.signal.priceStopLoss}`);
|
|
31
|
+
console.log(` Take Profit: ${event.signal.priceTakeProfit}`);
|
|
32
|
+
break;
|
|
33
|
+
|
|
34
|
+
case "closed":
|
|
35
|
+
console.log(`[POSITION CLOSED] ${event.symbol}`);
|
|
36
|
+
console.log(` Strategy: ${event.strategyName}`);
|
|
37
|
+
console.log(` Entry Price (adj): ${event.pnl.priceOpen}`);
|
|
38
|
+
console.log(` Exit Price (adj): ${event.pnl.priceClose}`);
|
|
39
|
+
console.log(` Signal ID: ${event.signal.id}`);
|
|
40
|
+
console.log(` Close Reason: ${event.closeReason}`);
|
|
41
|
+
console.log(` PnL: ${event.pnl.pnlPercentage.toFixed(2)}%`);
|
|
42
|
+
console.log(` Win: ${event.pnl.pnlPercentage > 0 ? "YES" : "NO"}`);
|
|
43
|
+
break;
|
|
44
|
+
|
|
45
|
+
case "cancelled":
|
|
46
|
+
console.log(`[POSITION CANCELLED] ${event.symbol}`);
|
|
47
|
+
console.log(` Strategy: ${event.strategyName}`);
|
|
48
|
+
console.log(` Signal ID: ${event.signal.id}`);
|
|
49
|
+
console.log(` Current Price: ${event.currentPrice}`);
|
|
50
|
+
console.log(` Cancel Reason: ${event.reason}`);
|
|
51
|
+
console.log(` Cancelled At: ${new Date(event.closeTimestamp).toISOString()}`);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default BacktestPositionMonitorAction;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CC_OLLAMA_API_KEY = process.env.CC_OLLAMA_API_KEY || "";
|
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,3 +1,5 @@
|
|
|
1
1
|
export default {
|
|
2
|
-
|
|
2
|
+
BacktestPartialProfitTakingAction: "backtest_partial_profit_taking_action",
|
|
3
|
+
BacktestLowerStopOnBreakevenAction: "backtest_lower_stop_on_breakeven_action",
|
|
4
|
+
BacktestPositionMonitorAction: "backtest_position_monitor_action",
|
|
3
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
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { addActionSchema } from "backtest-kit";
|
|
2
|
+
import ActionName from "../../enum/ActionName.mjs";
|
|
3
|
+
import { BacktestPartialProfitTakingAction } from "../../classes/BacktestPartialProfitTakingAction.mjs";
|
|
4
|
+
|
|
5
|
+
addActionSchema({
|
|
6
|
+
actionName: ActionName.BacktestPartialProfitTakingAction,
|
|
7
|
+
handler: BacktestPartialProfitTakingAction,
|
|
8
|
+
note: "Scale out at Kelly-optimized levels (33%, 33%, 34%)",
|
|
9
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { addActionSchema } from "backtest-kit";
|
|
2
|
+
import ActionName from "../../enum/ActionName.mjs";
|
|
3
|
+
import { BacktestPositionMonitorAction } from "../../classes/BacktestPositionMonitorAction.mjs";
|
|
4
|
+
|
|
5
|
+
addActionSchema({
|
|
6
|
+
actionName: ActionName.BacktestPositionMonitorAction,
|
|
7
|
+
handler: BacktestPositionMonitorAction,
|
|
8
|
+
note: "Monitors and logs position lifecycle events (open/close/scheduled)",
|
|
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,12 +1,15 @@
|
|
|
1
1
|
import "./exchange/binance.exchange.mjs";
|
|
2
2
|
|
|
3
|
-
import "./action/
|
|
3
|
+
import "./action/backtest_partial_profit_taking.action.mjs";
|
|
4
|
+
import "./action/backtest_lower_stop_on_breakeven.action.mjs";
|
|
5
|
+
import "./action/backtest_position_monitor.action.mjs";
|
|
4
6
|
|
|
5
7
|
import "./frame/dec_2025.frame.mjs";
|
|
6
8
|
import "./frame/nov_2025.frame.mjs";
|
|
7
9
|
import "./frame/oct_2025.frame.mjs";
|
|
10
|
+
import "./frame/feb_2024.frame.mjs";
|
|
8
11
|
|
|
9
|
-
import "./risk/
|
|
12
|
+
import "./risk/sl_distance.risk.mjs";
|
|
10
13
|
import "./risk/tp_distance.risk.mjs";
|
|
11
14
|
|
|
12
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,7 +1,8 @@
|
|
|
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";
|
|
@@ -10,20 +11,38 @@ addStrategySchema({
|
|
|
10
11
|
strategyName: StrategyName.MainStrategy,
|
|
11
12
|
interval: "5m",
|
|
12
13
|
getSignal: async (symbol) => {
|
|
13
|
-
|
|
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
|
+
}
|
|
14
36
|
|
|
15
37
|
{
|
|
16
|
-
|
|
38
|
+
math_15m.dumpPlot(signalId, symbol);
|
|
39
|
+
math_4h.dumpPlot(signalId, symbol);
|
|
17
40
|
}
|
|
18
41
|
|
|
19
|
-
return await
|
|
20
|
-
messages,
|
|
21
|
-
"glm-4.6:cloud",
|
|
22
|
-
process.env.CC_OLLAMA_API_KEY
|
|
23
|
-
);
|
|
42
|
+
return await math_15m.getSignal(signalId, symbol);
|
|
24
43
|
},
|
|
25
44
|
riskList: [
|
|
26
45
|
RiskName.TakeProfitDistanceRisk,
|
|
27
|
-
RiskName.
|
|
46
|
+
RiskName.StopLossDistanceRisk
|
|
28
47
|
],
|
|
29
48
|
});
|
package/src/main/bootstrap.mjs
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Backtest, listenError, overrideStrategySchema } from "backtest-kit";
|
|
2
|
+
|
|
3
|
+
import { getArgs } from "../utils/getArgs.mjs";
|
|
4
|
+
|
|
5
|
+
import ActionName from "../enum/ActionName.mjs";
|
|
6
|
+
|
|
7
|
+
const beginBacktest = async () => {
|
|
8
|
+
const { symbol, frameName, strategyName, exchangeName } = getArgs();
|
|
9
|
+
|
|
10
|
+
overrideStrategySchema({
|
|
11
|
+
strategyName,
|
|
12
|
+
actions: [
|
|
13
|
+
ActionName.BacktestPartialProfitTakingAction,
|
|
14
|
+
ActionName.BacktestLowerStopOnBreakevenAction,
|
|
15
|
+
ActionName.BacktestPositionMonitorAction,
|
|
16
|
+
],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
Backtest.background(symbol, {
|
|
20
|
+
strategyName,
|
|
21
|
+
frameName,
|
|
22
|
+
exchangeName,
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const beginPaper = async () => {
|
|
27
|
+
throw new Error("Todo: implement");
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const beginLive = async () => {
|
|
31
|
+
throw new Error("Todo: implement");
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const main = async () => {
|
|
35
|
+
const { backtest, live, paper } = getArgs();
|
|
36
|
+
|
|
37
|
+
if (backtest) {
|
|
38
|
+
await beginBacktest();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (paper) {
|
|
42
|
+
await beginPaper();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (live) {
|
|
46
|
+
await beginLive();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
listenError(console.log);
|
|
51
|
+
|
|
52
|
+
main();
|