oakscriptjs 0.13.1 → 0.14.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/AGENTS.md +53 -0
- package/README.md +1 -1
- package/bin/oakscript.mjs +5 -0
- package/dist/cjs/check/checks.d.ts +78 -0
- package/dist/cjs/check/checks.d.ts.map +1 -0
- package/dist/cjs/check/checks.js +109 -0
- package/dist/cjs/check/checks.js.map +7 -0
- package/dist/cjs/check/format.d.ts +26 -0
- package/dist/cjs/check/format.d.ts.map +1 -0
- package/dist/cjs/check/format.js +106 -0
- package/dist/cjs/check/format.js.map +7 -0
- package/dist/cjs/check/harness.d.ts +94 -0
- package/dist/cjs/check/harness.d.ts.map +1 -0
- package/dist/cjs/check/harness.js +302 -0
- package/dist/cjs/check/harness.js.map +7 -0
- package/dist/cjs/check/index.d.ts +253 -0
- package/dist/cjs/check/index.d.ts.map +1 -0
- package/dist/cjs/check/index.js +466 -0
- package/dist/cjs/check/index.js.map +7 -0
- package/dist/cjs/check/node.d.ts +53 -0
- package/dist/cjs/check/node.d.ts.map +1 -0
- package/dist/cjs/check/node.js +318 -0
- package/dist/cjs/check/node.js.map +7 -0
- package/dist/cjs/check/spec.d.ts +44 -0
- package/dist/cjs/check/spec.d.ts.map +1 -0
- package/dist/cjs/check/spec.js +199 -0
- package/dist/cjs/check/spec.js.map +7 -0
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/runtime/series.d.ts +6 -0
- package/dist/cjs/runtime/series.d.ts.map +1 -1
- package/dist/cjs/runtime/series.js +15 -0
- package/dist/cjs/runtime/series.js.map +2 -2
- package/dist/cjs/script/constants.d.ts +200 -0
- package/dist/cjs/script/constants.d.ts.map +1 -0
- package/dist/cjs/script/constants.js +217 -0
- package/dist/cjs/script/constants.js.map +7 -0
- package/dist/cjs/script/drawing-args.d.ts +25 -0
- package/dist/cjs/script/drawing-args.d.ts.map +1 -0
- package/dist/cjs/script/drawing-args.js +203 -0
- package/dist/cjs/script/drawing-args.js.map +7 -0
- package/dist/cjs/script/globals.d.ts +127 -0
- package/dist/cjs/script/globals.d.ts.map +1 -0
- package/dist/cjs/script/globals.js +17 -0
- package/dist/cjs/script/globals.js.map +7 -0
- package/dist/cjs/script/index.d.ts +499 -166
- package/dist/cjs/script/index.d.ts.map +1 -1
- package/dist/cjs/script/index.js +236 -96
- package/dist/cjs/script/index.js.map +3 -3
- package/dist/esm/check/checks.d.ts +78 -0
- package/dist/esm/check/checks.d.ts.map +1 -0
- package/dist/esm/check/checks.js +95 -0
- package/dist/esm/check/checks.js.map +1 -0
- package/dist/esm/check/format.d.ts +26 -0
- package/dist/esm/check/format.d.ts.map +1 -0
- package/dist/esm/check/format.js +105 -0
- package/dist/esm/check/format.js.map +1 -0
- package/dist/esm/check/harness.d.ts +94 -0
- package/dist/esm/check/harness.d.ts.map +1 -0
- package/dist/esm/check/harness.js +317 -0
- package/dist/esm/check/harness.js.map +1 -0
- package/dist/esm/check/index.d.ts +253 -0
- package/dist/esm/check/index.d.ts.map +1 -0
- package/dist/esm/check/index.js +470 -0
- package/dist/esm/check/index.js.map +1 -0
- package/dist/esm/check/node.d.ts +53 -0
- package/dist/esm/check/node.d.ts.map +1 -0
- package/dist/esm/check/node.js +347 -0
- package/dist/esm/check/node.js.map +1 -0
- package/dist/esm/check/spec.d.ts +44 -0
- package/dist/esm/check/spec.d.ts.map +1 -0
- package/dist/esm/check/spec.js +198 -0
- package/dist/esm/check/spec.js.map +1 -0
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/runtime/series.d.ts +6 -0
- package/dist/esm/runtime/series.d.ts.map +1 -1
- package/dist/esm/runtime/series.js +14 -0
- package/dist/esm/runtime/series.js.map +1 -1
- package/dist/esm/script/constants.d.ts +200 -0
- package/dist/esm/script/constants.d.ts.map +1 -0
- package/dist/esm/script/constants.js +151 -0
- package/dist/esm/script/constants.js.map +1 -0
- package/dist/esm/script/drawing-args.d.ts +25 -0
- package/dist/esm/script/drawing-args.d.ts.map +1 -0
- package/dist/esm/script/drawing-args.js +171 -0
- package/dist/esm/script/drawing-args.js.map +1 -0
- package/dist/esm/script/globals.d.ts +127 -0
- package/dist/esm/script/globals.d.ts.map +1 -0
- package/dist/esm/script/globals.js +28 -0
- package/dist/esm/script/globals.js.map +1 -0
- package/dist/esm/script/index.d.ts +499 -166
- package/dist/esm/script/index.d.ts.map +1 -1
- package/dist/esm/script/index.js +256 -83
- package/dist/esm/script/index.js.map +1 -1
- package/package.json +33 -1
- package/skills/oakscript/SKILL.md +198 -0
- package/skills/oakscript/examples/ma-cross-strategy.js +57 -0
- package/skills/oakscript/examples/ma-cross.js +34 -0
- package/skills/oakscript/examples/macd-oscillator.js +29 -0
- package/skills/oakscript/examples/mtf-trend-filter.js +46 -0
- package/skills/oakscript/reference/check.md +150 -0
- package/skills/oakscript/reference/errors.md +92 -0
- package/skills/oakscript/reference/pine-differences.md +85 -0
- package/skills/oakscript/reference/plotting.md +199 -0
- package/skills/oakscript/reference/script-structure.md +185 -0
- package/skills/oakscript/reference/strategies.md +140 -0
- package/skills/oakscript/reference/timeframes-symbols.md +97 -0
- package/skills/oakscript/spec.schema.json +199 -0
- package/src/check/checks.ts +175 -0
- package/src/check/format.ts +117 -0
- package/src/check/harness.ts +372 -0
- package/src/check/index.ts +708 -0
- package/src/check/node.ts +368 -0
- package/src/check/spec.ts +200 -0
- package/src/index.ts +1 -1
- package/src/runtime/series.ts +19 -0
- package/src/script/constants.ts +169 -0
- package/src/script/drawing-args.ts +179 -0
- package/src/script/globals.ts +129 -0
- package/src/script/index.ts +457 -81
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Runs a script source the way a host app runs a script written in its editor: the source is the body
|
|
3
|
+
* of the script, with every value export of `oakscriptjs/script` but `executeScript` as an implicit global, on
|
|
4
|
+
* deterministic synthetic bars (or given bars), with a chart context and, for a strategy, a small order engine.
|
|
5
|
+
* Used by {@link checkScript}, the `oakscript check` command and the tests of the agent guide.
|
|
6
|
+
*
|
|
7
|
+
* @module check/harness
|
|
8
|
+
*/
|
|
9
|
+
import * as lib from '../index.js';
|
|
10
|
+
import * as api from '../script/index.js';
|
|
11
|
+
import {
|
|
12
|
+
executeScript,
|
|
13
|
+
type ChartContext,
|
|
14
|
+
type ScriptRunResult,
|
|
15
|
+
type StrategyCloseOptions,
|
|
16
|
+
type StrategyDirection,
|
|
17
|
+
type StrategyEngine,
|
|
18
|
+
type StrategyEntryOptions,
|
|
19
|
+
type StrategyExitOptions,
|
|
20
|
+
type StrategyRiskRule,
|
|
21
|
+
type StrategySetup,
|
|
22
|
+
type StrategyTrade,
|
|
23
|
+
type StrategyVariable,
|
|
24
|
+
} from '../script/index.js';
|
|
25
|
+
import type { Bar } from '../types/index.js';
|
|
26
|
+
|
|
27
|
+
/** The implicit globals of a script: every value export of the script API but the host entry. */
|
|
28
|
+
export const SCRIPT_GLOBALS: readonly string[] = Object.keys(api).filter(
|
|
29
|
+
(k) => k !== 'default' && k !== 'executeScript' && /^[A-Za-z_$][\w$]*$/.test(k)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/** Name of the parameter that receives the `oakscriptjs` module in a compiled script (its `import` statements). */
|
|
33
|
+
export const LIBRARY_PARAMETER = '__oakscriptjs';
|
|
34
|
+
|
|
35
|
+
/** The values of {@link SCRIPT_GLOBALS}, then the `oakscriptjs` module, in the order of the compiled parameters. */
|
|
36
|
+
export function scriptArguments(): unknown[] {
|
|
37
|
+
return [...SCRIPT_GLOBALS.map((k) => (api as Record<string, unknown>)[k]), lib];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Rewrites the `import { a, b as c } from "oakscriptjs"` statements of a script into `const { a, b: c } = ...` on the
|
|
42
|
+
* same lines (the line numbers of the source do not change). Throws on an import of another module: a script runs
|
|
43
|
+
* self-contained.
|
|
44
|
+
*/
|
|
45
|
+
export function rewriteImports(source: string): string {
|
|
46
|
+
return source.replace(/^([ \t]*)import\s*(\{[^}]*\}|\*\s+as\s+[A-Za-z_$][\w$]*)\s*from\s*(['"])([^'"]+)\3\s*;?/gm, (all, indent, what, _q, from) => {
|
|
47
|
+
if (from !== 'oakscriptjs' && from !== 'oakscriptjs/script') {
|
|
48
|
+
throw new SyntaxError(`A script can only import from "oakscriptjs" (found "${from}").`);
|
|
49
|
+
}
|
|
50
|
+
const lines = all.split('\n').length - 1;
|
|
51
|
+
const binding = what.startsWith('*')
|
|
52
|
+
? what.replace(/^\*\s+as\s+/, '')
|
|
53
|
+
: what.replace(/\s+as\s+/g, ': ').replace(/\s+/g, ' ');
|
|
54
|
+
return `${indent}const ${binding} = ${LIBRARY_PARAMETER};${'\n'.repeat(lines)}`;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Start and end of the function the script source is wrapped in. The globals are the parameters of an outer function
|
|
60
|
+
* and the source is the body of an inner one, so a script can declare a name of a global (`const size = 2`), as in an
|
|
61
|
+
* app that imports the globals at the top of a module. The start goes on the first line of the source.
|
|
62
|
+
*/
|
|
63
|
+
export const SCRIPT_PREFIX = '"use strict"; return (function () { ';
|
|
64
|
+
export const SCRIPT_SUFFIX = '\n})();';
|
|
65
|
+
|
|
66
|
+
/** The source of the function body of a compiled script (imports rewritten, lines kept). */
|
|
67
|
+
export function wrapSource(source: string): string {
|
|
68
|
+
return SCRIPT_PREFIX + rewriteImports(source) + SCRIPT_SUFFIX;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Compiles a script source into a body for executeScript(); throws a SyntaxError on invalid code. */
|
|
72
|
+
export function compileScript(source: string): () => void {
|
|
73
|
+
const fn = new Function(...SCRIPT_GLOBALS, LIBRARY_PARAMETER, wrapSource(source)) as (...args: unknown[]) => void;
|
|
74
|
+
const values = scriptArguments();
|
|
75
|
+
return () => fn(...values);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const HOUR = 3600;
|
|
79
|
+
/** 2024-01-01 00:00 UTC, in seconds. */
|
|
80
|
+
const START = 1704067200;
|
|
81
|
+
|
|
82
|
+
/** Deterministic synthetic bars: two waves, a slow trend and seeded noise. `step`: seconds between bars. */
|
|
83
|
+
export function syntheticBars(count: number, step = HOUR, seed = 1): Bar[] {
|
|
84
|
+
let s = seed >>> 0 || 1;
|
|
85
|
+
const rand = (): number => {
|
|
86
|
+
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
|
|
87
|
+
return s / 2 ** 32;
|
|
88
|
+
};
|
|
89
|
+
const scale = Math.sqrt(step / HOUR);
|
|
90
|
+
const bars: Bar[] = [];
|
|
91
|
+
let prev = 100;
|
|
92
|
+
for (let i = 0; i < count; i++) {
|
|
93
|
+
const t = (i * step) / HOUR;
|
|
94
|
+
const close = 100 + 12 * Math.sin(t / 60) + 4 * Math.sin(t / 11) + t * 0.004 + (rand() - 0.5) * 1.5 * scale;
|
|
95
|
+
const open = prev;
|
|
96
|
+
const high = Math.max(open, close) + rand() * 0.8 * scale;
|
|
97
|
+
const low = Math.min(open, close) - rand() * 0.8 * scale;
|
|
98
|
+
bars.push({ time: START + i * step, open, high, low, close, volume: Math.round(1000 + rand() * 4000) });
|
|
99
|
+
prev = close;
|
|
100
|
+
}
|
|
101
|
+
return bars;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Seconds of a timeframe string ("1", "60", "240", "D", "1D", "W", "M"); NaN when it is not one. */
|
|
105
|
+
export function timeframeSeconds(timeframe: string): number {
|
|
106
|
+
const m = /^(\d*)([SDWM]?)$/.exec(timeframe.trim().toUpperCase());
|
|
107
|
+
if (!m) return NaN;
|
|
108
|
+
const n = m[1] ? Number(m[1]) : 1;
|
|
109
|
+
const unit = { '': 60, S: 1, D: 86400, W: 7 * 86400, M: 30 * 86400 }[m[2] as '' | 'S' | 'D' | 'W' | 'M'];
|
|
110
|
+
return n * unit;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The timeframe of bars from the most common spacing of their times ("60", "D", "W"...); "60" for fewer than 2 bars. */
|
|
114
|
+
export function inferTimeframe(bars: readonly Bar[]): string {
|
|
115
|
+
const counts = new Map<number, number>();
|
|
116
|
+
for (let i = 1; i < bars.length; i++) {
|
|
117
|
+
const d = bars[i]!.time - bars[i - 1]!.time;
|
|
118
|
+
if (d > 0) counts.set(d, (counts.get(d) ?? 0) + 1);
|
|
119
|
+
}
|
|
120
|
+
let step = 0;
|
|
121
|
+
let best = 0;
|
|
122
|
+
for (const [d, c] of counts) if (c > best) [step, best] = [d, c];
|
|
123
|
+
if (!step) return '60';
|
|
124
|
+
if (step >= 28 * 86400) return 'M';
|
|
125
|
+
if (step >= 7 * 86400) return 'W';
|
|
126
|
+
if (step >= 86400) return 'D';
|
|
127
|
+
if (step < 60) return `${step}S`;
|
|
128
|
+
return String(Math.round(step / 60));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface TestChartOptions {
|
|
132
|
+
/** Chart timeframe (default "60"). */
|
|
133
|
+
timeframe?: string;
|
|
134
|
+
/** Chart ticker id (default "TEST:SYN"). */
|
|
135
|
+
tickerid?: string;
|
|
136
|
+
/** Time zone of the chart and of the other symbols (default "Etc/UTC"). */
|
|
137
|
+
timezone?: string;
|
|
138
|
+
/** Session of the chart and of the other symbols (default "24x7"). */
|
|
139
|
+
session?: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Chart context of the test runs: a 24x7 symbol in UTC. Higher timeframes of the chart symbol are resampled from the
|
|
144
|
+
* chart bars by the library; other symbols get synthetic bars of their own (seeded by the symbol name).
|
|
145
|
+
*/
|
|
146
|
+
export function testChart(bars: Bar[], options: TestChartOptions = {}): ChartContext {
|
|
147
|
+
const timezone = options.timezone ?? 'Etc/UTC';
|
|
148
|
+
const session = options.session ?? '24x7';
|
|
149
|
+
const chartStep = timeframeSeconds(options.timeframe ?? '60') || HOUR;
|
|
150
|
+
const span = bars.length ? bars[bars.length - 1]!.time - bars[0]!.time + chartStep : 0;
|
|
151
|
+
return {
|
|
152
|
+
tickerid: options.tickerid ?? 'TEST:SYN',
|
|
153
|
+
timeframe: options.timeframe ?? '60',
|
|
154
|
+
timezone,
|
|
155
|
+
session,
|
|
156
|
+
mintick: 0.01,
|
|
157
|
+
pointvalue: 1,
|
|
158
|
+
mincontract: 1,
|
|
159
|
+
currency: 'USD',
|
|
160
|
+
bars: (symbol, timeframe) => {
|
|
161
|
+
const step = timeframeSeconds(timeframe);
|
|
162
|
+
if (!step) return [];
|
|
163
|
+
// a run on zero bars (the declaration run of a host) gets one empty bar: no bars at all is an invalid symbol
|
|
164
|
+
if (!bars.length) return [{ time: 0, open: 0, high: 0, low: 0, close: 0, volume: 0 }];
|
|
165
|
+
const first = bars[0]!.time;
|
|
166
|
+
return syntheticBars(Math.ceil(span / step), step, symbol.length + 7).map((b) => ({
|
|
167
|
+
...b,
|
|
168
|
+
time: first + (b.time - START),
|
|
169
|
+
}));
|
|
170
|
+
},
|
|
171
|
+
symbolInfo: () => ({ timezone, session, mintick: 0.01, currency: 'USD' }),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* A small order engine for checks and tests (the library does not fill orders). Market entries and closes fill at the
|
|
177
|
+
* next bar open; `strategy.exit` stop / limit levels are checked inside each bar (stop first when both are hit).
|
|
178
|
+
* Entry limit / stop prices, pyramiding and process_orders_on_close are not simulated.
|
|
179
|
+
*/
|
|
180
|
+
export class TestEngine implements StrategyEngine {
|
|
181
|
+
readonly closed: StrategyTrade[] = [];
|
|
182
|
+
private open: StrategyTrade | null = null;
|
|
183
|
+
private pendingEntry: { id: string; direction: StrategyDirection; qty?: number } | null = null;
|
|
184
|
+
private pendingClose = false;
|
|
185
|
+
private exits = new Map<string, StrategyExitOptions>();
|
|
186
|
+
private net = 0;
|
|
187
|
+
private bar = 0;
|
|
188
|
+
|
|
189
|
+
constructor(readonly setup: StrategySetup) {}
|
|
190
|
+
|
|
191
|
+
private get bars(): readonly Bar[] {
|
|
192
|
+
return this.setup.bars;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private equity(price: number): number {
|
|
196
|
+
return this.setup.properties.initial_capital + this.net + this.openProfit(price);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private openProfit(price: number): number {
|
|
200
|
+
return this.open ? (price - this.open.entry_price) * this.open.size : 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private commission(value: number): number {
|
|
204
|
+
const p = this.setup.properties;
|
|
205
|
+
return p.commission_type === 'percent' ? (Math.abs(value) * p.commission_value) / 100 : 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private closeAt(price: number, exitId: string): void {
|
|
209
|
+
const t = this.open;
|
|
210
|
+
if (!t) return;
|
|
211
|
+
const commission = t.commission + this.commission(price * t.size);
|
|
212
|
+
const profit = (price - t.entry_price) * t.size - commission;
|
|
213
|
+
this.net += profit;
|
|
214
|
+
this.closed.push({
|
|
215
|
+
...t,
|
|
216
|
+
commission,
|
|
217
|
+
profit,
|
|
218
|
+
profit_percent: (profit / Math.abs(t.entry_price * t.size)) * 100,
|
|
219
|
+
exit_id: exitId,
|
|
220
|
+
exit_price: price,
|
|
221
|
+
exit_bar_index: this.bar,
|
|
222
|
+
exit_time: this.bars[this.bar]!.time * 1000,
|
|
223
|
+
});
|
|
224
|
+
this.open = null;
|
|
225
|
+
this.exits.clear();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** The open trade, if any. */
|
|
229
|
+
get openTradeNow(): StrategyTrade | null {
|
|
230
|
+
return this.open;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
processBar(bar: number): void {
|
|
234
|
+
this.bar = bar;
|
|
235
|
+
const b = this.bars[bar]!;
|
|
236
|
+
if (this.pendingClose) this.closeAt(b.open, 'Close');
|
|
237
|
+
this.pendingClose = false;
|
|
238
|
+
const e = this.pendingEntry;
|
|
239
|
+
this.pendingEntry = null;
|
|
240
|
+
if (e && !(this.open && Math.sign(this.open.size) === (e.direction === 'long' ? 1 : -1))) {
|
|
241
|
+
if (this.open) this.closeAt(b.open, e.id);
|
|
242
|
+
const p = this.setup.properties;
|
|
243
|
+
const equity = this.equity(b.open);
|
|
244
|
+
const qty =
|
|
245
|
+
e.qty ??
|
|
246
|
+
(p.default_qty_type === 'fixed'
|
|
247
|
+
? p.default_qty_value
|
|
248
|
+
: p.default_qty_type === 'cash'
|
|
249
|
+
? p.default_qty_value / b.open
|
|
250
|
+
: (equity * p.default_qty_value) / 100 / b.open);
|
|
251
|
+
const size = e.direction === 'long' ? qty : -qty;
|
|
252
|
+
this.open = {
|
|
253
|
+
entry_id: e.id,
|
|
254
|
+
entry_price: b.open,
|
|
255
|
+
entry_bar_index: bar,
|
|
256
|
+
entry_time: b.time * 1000,
|
|
257
|
+
size,
|
|
258
|
+
profit: 0,
|
|
259
|
+
profit_percent: 0,
|
|
260
|
+
commission: this.commission(b.open * size),
|
|
261
|
+
max_runup: 0,
|
|
262
|
+
max_runup_percent: 0,
|
|
263
|
+
max_drawdown: 0,
|
|
264
|
+
max_drawdown_percent: 0,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
const t = this.open;
|
|
268
|
+
if (!t) return;
|
|
269
|
+
for (const [id, x] of this.exits) {
|
|
270
|
+
if (x.from_entry !== undefined && x.from_entry !== t.entry_id) continue;
|
|
271
|
+
const long = t.size > 0;
|
|
272
|
+
if (x.stop !== undefined && (long ? b.low <= x.stop : b.high >= x.stop)) {
|
|
273
|
+
this.closeAt(long ? Math.min(b.open, x.stop) : Math.max(b.open, x.stop), id);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (x.limit !== undefined && (long ? b.high >= x.limit : b.low <= x.limit)) {
|
|
277
|
+
this.closeAt(long ? Math.max(b.open, x.limit) : Math.min(b.open, x.limit), id);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
processClose(): void {}
|
|
284
|
+
|
|
285
|
+
entry(id: string, direction: StrategyDirection, options: StrategyEntryOptions): void {
|
|
286
|
+
this.pendingEntry = { id, direction, qty: options.qty };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
order(id: string, direction: StrategyDirection, options: StrategyEntryOptions): void {
|
|
290
|
+
this.entry(id, direction, options);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
exit(id: string, options: StrategyExitOptions): void {
|
|
294
|
+
this.exits.set(id, options);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
close(id: string, _options: StrategyCloseOptions): void {
|
|
298
|
+
if (this.open?.entry_id === id) this.pendingClose = true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
close_all(): void {
|
|
302
|
+
if (this.open) this.pendingClose = true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
cancel(id: string): void {
|
|
306
|
+
this.exits.delete(id);
|
|
307
|
+
if (this.pendingEntry?.id === id) this.pendingEntry = null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
cancel_all(): void {
|
|
311
|
+
this.exits.clear();
|
|
312
|
+
this.pendingEntry = null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
readonly rules: StrategyRiskRule[] = [];
|
|
316
|
+
|
|
317
|
+
risk(rule: StrategyRiskRule): void {
|
|
318
|
+
this.rules.push(rule);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
get(variable: StrategyVariable): number | string | undefined {
|
|
322
|
+
const price = this.bars[this.bar]?.close ?? NaN;
|
|
323
|
+
const wins = this.closed.filter((t) => t.profit > 0).length;
|
|
324
|
+
const values: Partial<Record<StrategyVariable, number | string>> = {
|
|
325
|
+
position_size: this.open?.size ?? 0,
|
|
326
|
+
position_avg_price: this.open?.entry_price ?? NaN,
|
|
327
|
+
initial_capital: this.setup.properties.initial_capital,
|
|
328
|
+
equity: this.equity(price),
|
|
329
|
+
netprofit: this.net,
|
|
330
|
+
openprofit: this.openProfit(price),
|
|
331
|
+
opentrades: this.open ? 1 : 0,
|
|
332
|
+
closedtrades: this.closed.length,
|
|
333
|
+
wintrades: wins,
|
|
334
|
+
losstrades: this.closed.length - wins,
|
|
335
|
+
account_currency: 'USD',
|
|
336
|
+
};
|
|
337
|
+
return values[variable];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
openTrade(index: number): StrategyTrade | undefined {
|
|
341
|
+
return index === 0 && this.open ? this.open : undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
closedTrade(index: number): StrategyTrade | undefined {
|
|
345
|
+
return this.closed[index];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export interface ScriptRun {
|
|
350
|
+
run: ScriptRunResult;
|
|
351
|
+
engine?: TestEngine;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export interface RunScriptOptions extends TestChartOptions {
|
|
355
|
+
/** Compiles the source (default {@link compileScript}); the CLI compiles with file lines. */
|
|
356
|
+
compile?: (source: string) => () => void;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Runs a script source on `bars` with the test chart context and, for a strategy, the test engine. */
|
|
360
|
+
export function runScript(
|
|
361
|
+
source: string,
|
|
362
|
+
bars: Bar[],
|
|
363
|
+
inputs: Record<string, unknown> = {},
|
|
364
|
+
options: RunScriptOptions = {}
|
|
365
|
+
): ScriptRun {
|
|
366
|
+
let engine: TestEngine | undefined;
|
|
367
|
+
const body = (options.compile ?? compileScript)(source);
|
|
368
|
+
const run = executeScript(body, bars, inputs, testChart(bars, options), {
|
|
369
|
+
strategyEngine: (setup) => (engine = new TestEngine(setup)),
|
|
370
|
+
});
|
|
371
|
+
return { run, engine };
|
|
372
|
+
}
|