oakscriptjs 0.13.0 → 0.13.2
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 +48 -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/script/index.d.ts.map +1 -1
- package/dist/cjs/script/index.js +42 -12
- package/dist/cjs/script/index.js.map +2 -2
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/script/index.d.ts.map +1 -1
- package/dist/esm/script/index.js +57 -12
- package/dist/esm/script/index.js.map +1 -1
- package/package.json +3 -1
- package/skills/oakscript/SKILL.md +162 -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/errors.md +68 -0
- package/skills/oakscript/reference/pine-differences.md +84 -0
- package/skills/oakscript/reference/plotting.md +193 -0
- package/skills/oakscript/reference/script-structure.md +182 -0
- package/skills/oakscript/reference/strategies.md +140 -0
- package/skills/oakscript/reference/timeframes-symbols.md +93 -0
- package/src/index.ts +1 -1
- package/src/script/index.ts +50 -13
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Strategies
|
|
2
|
+
|
|
3
|
+
A strategy is a script declared with `strategy()` whose orders go to an order engine. OakScriptJS does not fill
|
|
4
|
+
orders itself: the host supplies the engine (an app's backtester, or your own), which fills the orders, keeps the
|
|
5
|
+
trades and computes the results the host shows (net profit, drawdown, list of trades...).
|
|
6
|
+
|
|
7
|
+
## Declaration and sizing
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
strategy("Breakout", {
|
|
11
|
+
overlay: true, // default false
|
|
12
|
+
initial_capital: 10000, // default 100000
|
|
13
|
+
default_qty_type: strategy.percent_of_equity, // or strategy.fixed (units), strategy.cash (amount)
|
|
14
|
+
default_qty_value: 25, // default 100 (% of equity)
|
|
15
|
+
pyramiding: 0, // entries allowed in the same direction beyond the first
|
|
16
|
+
commission_type: strategy.commission.percent, // or .cash_per_contract, .cash_per_order
|
|
17
|
+
commission_value: 0.1,
|
|
18
|
+
slippage: 2, // ticks added to market and stop fills
|
|
19
|
+
});
|
|
20
|
+
const length = input.int(20, "Channel length", { minval: 2 });
|
|
21
|
+
const upper = ta.highest(high, length).offset(1);
|
|
22
|
+
const lower = ta.lowest(low, length).offset(1);
|
|
23
|
+
plot(upper, "Upper", { color: color.green });
|
|
24
|
+
plot(lower, "Lower", { color: color.red });
|
|
25
|
+
strategy.eachBar((c) => {
|
|
26
|
+
if (strategy.position_size <= 0) strategy.entry("Long", strategy.long, { stop: c.get(upper) });
|
|
27
|
+
if (strategy.position_size >= 0) strategy.entry("Short", strategy.short, { stop: c.get(lower) });
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Other properties, with the Pine Script v6 names and defaults: `currency`, `margin_long` / `margin_short` (100),
|
|
32
|
+
`process_orders_on_close` (false), `close_entries_rule` (`"FIFO"`), `calc_on_order_fills`, `calc_on_every_tick`,
|
|
33
|
+
`use_bar_magnifier`, `fill_orders_on_standard_ohlc`, `backtest_fill_limits_assumption`, `risk_free_rate` (2).
|
|
34
|
+
A host can override them from its settings.
|
|
35
|
+
|
|
36
|
+
Order size: the `qty` option of an order (units) wins over `default_qty_type` / `default_qty_value`.
|
|
37
|
+
|
|
38
|
+
## The bar loop
|
|
39
|
+
|
|
40
|
+
`strategy.eachBar((c) => ...)` is the strategy's `eachBar` (same `c`, returns a Series) and runs once per script.
|
|
41
|
+
On each bar:
|
|
42
|
+
|
|
43
|
+
1. the engine fills the orders placed before (market orders at the bar's open; stop / limit orders when the
|
|
44
|
+
bar reaches their price);
|
|
45
|
+
2. the callback runs at the bar's close: read the state, place, change or cancel orders;
|
|
46
|
+
3. with `process_orders_on_close: true`, new market orders fill at that close; else at the next bar's open.
|
|
47
|
+
|
|
48
|
+
Order calls and per-bar `strategy.*` reads throw outside `strategy.eachBar()`. Compute every Series before it.
|
|
49
|
+
|
|
50
|
+
## Orders
|
|
51
|
+
|
|
52
|
+
| Call | Use |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `strategy.entry(id, strategy.long / strategy.short, { qty, limit, stop, oca_name, oca_type, comment, alert_message })` | open a position; an entry in the other direction reverses the position; market order without `limit` / `stop` |
|
|
55
|
+
| `strategy.order(id, direction, { qty, limit, stop, ... })` | a raw order: adds to or reduces the position, no reversal logic |
|
|
56
|
+
| `strategy.exit(id, { from_entry, qty, qty_percent, stop, limit, loss, profit, trail_price, trail_points, trail_offset, comment, ... })` | stop loss / take profit / trailing stop for an entry; `stop` / `limit` / `trail_price` are prices, `loss` / `profit` / `trail_points` / `trail_offset` are in ticks |
|
|
57
|
+
| `strategy.close(id, { qty, qty_percent, comment, immediately })` | close the trades of entry `id` at market |
|
|
58
|
+
| `strategy.close_all({ comment, immediately })` | close the whole position at market |
|
|
59
|
+
| `strategy.cancel(id)`, `strategy.cancel_all()` | cancel pending orders |
|
|
60
|
+
|
|
61
|
+
- A call with the id of a pending order changes that order (calling `strategy.exit` on every bar with new prices
|
|
62
|
+
moves the stop).
|
|
63
|
+
- `NaN` in an option means "not given" (it is removed), as `na` in Pine.
|
|
64
|
+
- `strategy.exit` placed while the entry is still pending waits for it (`from_entry`).
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
strategy("Trailing exit", { overlay: true, initial_capital: 10000, default_qty_type: strategy.fixed, default_qty_value: 10 });
|
|
68
|
+
const fast = ta.ema(close, 12);
|
|
69
|
+
const slow = ta.ema(close, 26);
|
|
70
|
+
const atr = ta.atr(14);
|
|
71
|
+
const goLong = ta.crossover(fast, slow);
|
|
72
|
+
const goFlat = ta.crossunder(fast, slow);
|
|
73
|
+
strategy.eachBar((c) => {
|
|
74
|
+
if (c.get(goLong) === 1) strategy.entry("Long", strategy.long, { comment: "EMA cross" });
|
|
75
|
+
if (c.get(goFlat) === 1) strategy.close("Long", { comment: "Cross down" });
|
|
76
|
+
if (strategy.position_size > 0) {
|
|
77
|
+
const trade = strategy.opentrade(0); // Pine strategy.opentrades.*(0)
|
|
78
|
+
const stop = trade.entry_price - 2 * c.get(atr);
|
|
79
|
+
strategy.exit("Stop", { from_entry: "Long", stop, comment: "ATR stop" });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
plot(fast, "Fast");
|
|
83
|
+
plot(slow, "Slow");
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Reading the results
|
|
87
|
+
|
|
88
|
+
Inside `strategy.eachBar()`, from the engine, for the current bar:
|
|
89
|
+
|
|
90
|
+
| Variable | Meaning |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `strategy.position_size` | signed quantity: > 0 long, < 0 short, 0 flat |
|
|
93
|
+
| `strategy.position_avg_price`, `strategy.position_entry_name` | average entry price (NaN when flat), entry id |
|
|
94
|
+
| `strategy.equity`, `strategy.initial_capital` | initial capital + net profit + open profit |
|
|
95
|
+
| `strategy.netprofit`, `strategy.openprofit`, `strategy.grossprofit`, `strategy.grossloss` (and `_percent`) | profits |
|
|
96
|
+
| `strategy.opentrades`, `strategy.closedtrades`, `strategy.wintrades`, `strategy.losstrades`, `strategy.eventrades` | trade counts |
|
|
97
|
+
| `strategy.max_drawdown`, `strategy.max_runup`, `strategy.avg_trade`... | statistics |
|
|
98
|
+
| `strategy.opentrade(i)`, `strategy.closedtrade(i)` | trade `i` (0 = oldest): `{ entry_id, entry_price, entry_bar_index, entry_time, size, profit, commission, exit_price, exit_bar_index, ... }`, undefined when there is none |
|
|
99
|
+
| `strategy.default_entry_qty(price)` | the quantity a default-size entry would get |
|
|
100
|
+
|
|
101
|
+
Pine's `strategy.closedtrades.profit(i)` is `strategy.closedtrade(i)?.profit`. `strategy.initial_capital` and
|
|
102
|
+
`strategy.account_currency` can be read anywhere.
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
strategy("Equity curve", { overlay: false, initial_capital: 5000, default_qty_type: strategy.cash, default_qty_value: 1000 });
|
|
106
|
+
strategy.risk.allow_entry_in(strategy.direction.long); // risk rules: top level, after strategy()
|
|
107
|
+
const rsi = ta.rsi(close, 14);
|
|
108
|
+
const equity = strategy.eachBar((c) => {
|
|
109
|
+
if (c.get(rsi) < 30) strategy.entry("Long", strategy.long);
|
|
110
|
+
if (c.get(rsi) > 70) strategy.close_all({ comment: "RSI > 70" });
|
|
111
|
+
const last = strategy.closedtrade(strategy.closedtrades - 1);
|
|
112
|
+
if (last && last.exit_bar_index === c.i && last.profit < 0) alert("Losing trade closed");
|
|
113
|
+
return strategy.equity; // the loop's Series: equity per bar
|
|
114
|
+
});
|
|
115
|
+
plot(equity, "Equity", { color: color.teal });
|
|
116
|
+
hline(5000, "Initial capital", { color: color.gray });
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Risk rules: `strategy.risk.allow_entry_in(dir)`, `max_drawdown(value, type)`, `max_intraday_loss(value, type)`,
|
|
120
|
+
`max_cons_loss_days(count)`, `max_intraday_filled_orders(count)`, `max_position_size(contracts)` (type:
|
|
121
|
+
`"percent_of_equity"` or `"cash"`). An engine that does not support a rule or a variable throws
|
|
122
|
+
"... is not provided by the strategy engine".
|
|
123
|
+
|
|
124
|
+
## The host side
|
|
125
|
+
|
|
126
|
+
A host passes an engine factory to `executeScript`:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { executeScript, type StrategyEngine } from 'oakscriptjs/script';
|
|
130
|
+
|
|
131
|
+
const run = executeScript(body, bars, inputs, chart, {
|
|
132
|
+
strategyEngine: ({ properties, bars, chart }) => new MyEngine(properties, bars), // implements StrategyEngine
|
|
133
|
+
});
|
|
134
|
+
// run.strategyConfig: the resolved properties
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`StrategyEngine` methods: `processBar(i)`, `processClose(i)`, `entry`, `order`, `exit`, `close`, `close_all`,
|
|
138
|
+
`cancel`, `cancel_all`, `get(variable)`, `openTrade(i)`, `closedTrade(i)`, and optionally `risk(rule)`,
|
|
139
|
+
`defaultEntryQty(price)`. The option objects keep the Pine names. Without an engine, `strategy()` declares the
|
|
140
|
+
strategy and `strategy.eachBar()` throws "no strategy engine".
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Other timeframes and symbols
|
|
2
|
+
|
|
3
|
+
## request.security
|
|
4
|
+
|
|
5
|
+
`request.security(symbol, timeframe, expression, gaps?, lookahead?, ignore_invalid_symbol?)` runs `expression` on
|
|
6
|
+
the bars of `symbol` on `timeframe` and returns its values aligned on the chart bars (a Series).
|
|
7
|
+
|
|
8
|
+
- `symbol`: `""` or `syminfo.tickerid` for the chart symbol, a ticker id (`"NASDAQ:AAPL"`, from
|
|
9
|
+
`input.symbol`) for another one, `ticker.heikinashi(syminfo.tickerid)` for Heikin Ashi bars.
|
|
10
|
+
- `timeframe`: `"1"`, `"5"`, `"60"`, `"240"`, `"D"`, `"W"`, `"M"`, `"2D"`...; `""` is the chart timeframe.
|
|
11
|
+
- `expression`: a **function** that computes the series. Everything it needs on that timeframe is computed
|
|
12
|
+
inside it: `() => ta.ema(close, 20)`. A Series computed outside runs on the chart bars, and the call throws
|
|
13
|
+
"the expression returned a series computed on the chart bars".
|
|
14
|
+
- Return an array for several values (a Pine tuple): `const [h, l] = request.security("", "D", () => [high, low]);`
|
|
15
|
+
- Inputs and plain numbers from outside can be used inside (`() => ta.ema(close, length)`).
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
indicator("Daily levels", { overlay: true });
|
|
19
|
+
const length = input.int(20, "Daily EMA length", { minval: 1 });
|
|
20
|
+
const dailyEma = request.security("", "D", () => ta.ema(close, length));
|
|
21
|
+
const [prevHigh, prevLow] = request.security("", "D", () => [high.offset(1), low.offset(1)]);
|
|
22
|
+
plot(dailyEma, "Daily EMA", { color: color.orange, style: plot.style_stepline });
|
|
23
|
+
plot(prevHigh, "Previous day high", { color: color.green, style: plot.style_linebr });
|
|
24
|
+
plot(prevLow, "Previous day low", { color: color.red, style: plot.style_linebr });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Lookahead and gaps
|
|
28
|
+
|
|
29
|
+
- `barmerge.lookahead_off` (default): a higher-timeframe value appears on the chart bar that completes its
|
|
30
|
+
period; before that, the bars show the previous period's value. This is the setting without future leak.
|
|
31
|
+
- `barmerge.lookahead_on`: the value of the period appears from its first chart bar. With a series that is not
|
|
32
|
+
offset (`close`, `ta.ema(close, 20)`) this reads the future on history bars: use it only with
|
|
33
|
+
`high.offset(1)`-style expressions (the previous period) or for display.
|
|
34
|
+
- `barmerge.gaps_on`: the value only on the bar where it appears, NaN elsewhere (use `plot.style_linebr`).
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
indicator("Weekly open", { overlay: true });
|
|
38
|
+
const weekOpen = request.security("", "W", () => open, barmerge.gaps_off, barmerge.lookahead_on);
|
|
39
|
+
plot(weekOpen, "Week open", { color: color.purple, style: plot.style_stepline });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Other symbols
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
indicator("Relative strength", { overlay: false });
|
|
46
|
+
const other = input.symbol("NASDAQ:AAPL", "Compare with");
|
|
47
|
+
const otherClose = request.security(other, "", () => close);
|
|
48
|
+
const ratio = close.div(otherClose);
|
|
49
|
+
plot(ratio, "Ratio", { color: color.blue });
|
|
50
|
+
plot(ta.sma(ratio, 50), "Ratio SMA", { color: color.orange });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The library fetches nothing: the host gives the bars of the other symbol. An app loads them when the script asks
|
|
54
|
+
(the first run reports what it needs, the app loads it and runs again); in your own `executeScript` call, pass
|
|
55
|
+
`bars(symbol, timeframe)` and `symbolInfo(symbol)` in the chart context and catch `MissingSymbolDataError`
|
|
56
|
+
(`error.requests` lists the `{ symbol, timeframe }` to load). A symbol with no bars stops the script with
|
|
57
|
+
"Invalid symbol: X" (or gives NaN with `ignore_invalid_symbol = true`).
|
|
58
|
+
|
|
59
|
+
The values are merged by time: with lookahead off a chart bar shows the last bar of the other symbol that closed
|
|
60
|
+
at or before its own close.
|
|
61
|
+
|
|
62
|
+
### Lower timeframes
|
|
63
|
+
|
|
64
|
+
`request.security` on a timeframe below the chart's reads the lower-timeframe bars (given by the host as for
|
|
65
|
+
another symbol) and shows, per chart bar, the value on the last lower bar inside it.
|
|
66
|
+
`request.security_lower_tf(symbol, timeframe, expression)` returns, for each chart bar, an **array of numbers**:
|
|
67
|
+
the values on all the lower bars inside that bar (a plain `number[][]`, not a Series). Read it in a bar loop:
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
indicator("Intrabar up volume", { overlay: false });
|
|
71
|
+
const vols = request.security_lower_tf("", "15", () => volume.mul(close.gt(open).iff(1, 0)));
|
|
72
|
+
const upVolume = eachBar((c) => vols[c.i].reduce((sum, v) => sum + v, 0));
|
|
73
|
+
plot(upVolume, "Up volume", { style: plot.style_columns, color: color.teal });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Timeframe values
|
|
77
|
+
|
|
78
|
+
- `timeframe.period` (`"60"`, `"1D"`...), `timeframe.multiplier`, `timeframe.isintraday`, `timeframe.isdaily`,
|
|
79
|
+
`timeframe.isweekly`, `timeframe.ismonthly`, `timeframe.isdwm`, `timeframe.isminutes`, `timeframe.isseconds`.
|
|
80
|
+
- `timeframe.change("D")`: 1 on the first chart bar of each new period.
|
|
81
|
+
- `timeframe.in_seconds(tf?)`, `timeframe.from_seconds(s)`.
|
|
82
|
+
- `input.timeframe("D", "Timeframe")` lets the user choose; `""` is the chart timeframe.
|
|
83
|
+
- `time("D")`, `time("240", "0930-1600")`: open time of the period of each bar, NaN outside the session.
|
|
84
|
+
|
|
85
|
+
A timeframe below the chart's with `request.security_lower_tf` and a higher one with `request.security` are the
|
|
86
|
+
normal cases; `request.security_lower_tf` on a higher timeframe throws.
|
|
87
|
+
|
|
88
|
+
## What the host must give
|
|
89
|
+
|
|
90
|
+
Session-aware values (`request.security` on another timeframe, `time()`, `timeframe.change`, `session.*`,
|
|
91
|
+
`ta.vwap` on an intraday chart) need the chart timeframe, the exchange time zone and the session. An app gives
|
|
92
|
+
them; a direct `executeScript` call passes `{ timeframe, timezone, session }` in the chart context
|
|
93
|
+
(`session: "24x7"` for a symbol that always trades, `"0930-1600"` for regular stock hours).
|
package/src/index.ts
CHANGED
|
@@ -224,7 +224,7 @@ export type { LightweightSeriesDefinitions } from './runtime/adapters/Lightweigh
|
|
|
224
224
|
export { SimpleInputAdapter } from './runtime/adapters/SimpleInputAdapter.js';
|
|
225
225
|
|
|
226
226
|
// Version
|
|
227
|
-
export const VERSION = '0.
|
|
227
|
+
export const VERSION = '0.13.2';
|
|
228
228
|
|
|
229
229
|
/**
|
|
230
230
|
* Library information
|
package/src/script/index.ts
CHANGED
|
@@ -338,6 +338,9 @@ export interface ScriptOptions {
|
|
|
338
338
|
|
|
339
339
|
let scriptOptions: ScriptOptions = {};
|
|
340
340
|
|
|
341
|
+
/** The `varip` values of a replayed bar in a script without `varip`. */
|
|
342
|
+
const NO_VARIP: unknown[] = [];
|
|
343
|
+
|
|
341
344
|
/** Intrabar run of the current run (see {@link executeIntrabar}); null in a main run. */
|
|
342
345
|
interface IntrabarRun {
|
|
343
346
|
/** The bar calculated inside the bar: the last bar of the run. */
|
|
@@ -474,7 +477,7 @@ export function executeScript(
|
|
|
474
477
|
barColors: c.barColors.length ? c.barColors : undefined,
|
|
475
478
|
plotCandles: Object.keys(c.plotCandles).length ? c.plotCandles : undefined,
|
|
476
479
|
plotBars: Object.keys(c.plotBars).length ? c.plotBars : undefined,
|
|
477
|
-
...drawingOutputs(ctxBars.bars, chartCtx.timeUnit),
|
|
480
|
+
...(intrabarRun ? {} : drawingOutputs(ctxBars.bars, chartCtx.timeUnit)),
|
|
478
481
|
alerts: c.alerts.length ? c.alerts : undefined,
|
|
479
482
|
alertCalls: c.alertCalls.length ? c.alertCalls : undefined,
|
|
480
483
|
},
|
|
@@ -943,6 +946,7 @@ export const DEFAULT_OUTPUT_TITLES = {
|
|
|
943
946
|
|
|
944
947
|
/** PineScript `plot()` — declares the plot and supplies its data in one call. */
|
|
945
948
|
function plotImpl(series: Series, title: string = DEFAULT_OUTPUT_TITLES.plot, options: ScriptPlotOptions = {}): PlotHandle {
|
|
949
|
+
if (intrabarRun) return { id: '', kind: 'plot' }; // dropped: an intrabar run's outputs aren't read
|
|
946
950
|
const c = collector();
|
|
947
951
|
const id = `plot${c.plotConfig.length}`;
|
|
948
952
|
const perBar = Array.isArray(options.color) ? options.color : undefined;
|
|
@@ -1077,6 +1081,7 @@ export function fill(
|
|
|
1077
1081
|
...rest: unknown[]
|
|
1078
1082
|
): void;
|
|
1079
1083
|
export function fill(a: PlotHandle, b: PlotHandle, ...args: unknown[]): void {
|
|
1084
|
+
if (intrabarRun) return; // dropped: an intrabar run's outputs aren't read
|
|
1080
1085
|
const c = collector();
|
|
1081
1086
|
const third = args[0];
|
|
1082
1087
|
let perBar: string[] | undefined;
|
|
@@ -1219,6 +1224,7 @@ function emitMarkers(
|
|
|
1219
1224
|
title: string | undefined,
|
|
1220
1225
|
options: PlotShapeOptions & PlotCharOptions
|
|
1221
1226
|
): void {
|
|
1227
|
+
if (intrabarRun) return; // dropped: an intrabar run's outputs aren't read
|
|
1222
1228
|
const c = collector();
|
|
1223
1229
|
const id = `${kind}${c.shapeConfig.filter((s) => s.kind === kind).length}`;
|
|
1224
1230
|
title ??= DEFAULT_OUTPUT_TITLES[kind];
|
|
@@ -1302,6 +1308,7 @@ export interface PlotArrowOptions {
|
|
|
1302
1308
|
* `arrowConfig`; the renderer scales the height with the absolute value between minheight and maxheight.
|
|
1303
1309
|
*/
|
|
1304
1310
|
export function plotarrow(series: Series, title?: string, options: PlotArrowOptions = {}): void {
|
|
1311
|
+
if (intrabarRun) return; // dropped: an intrabar run's outputs aren't read
|
|
1305
1312
|
const c = collector();
|
|
1306
1313
|
const id = `arrow${c.arrowConfig.length}`;
|
|
1307
1314
|
title ??= DEFAULT_OUTPUT_TITLES.arrow;
|
|
@@ -1345,6 +1352,7 @@ function emitBarColors(
|
|
|
1345
1352
|
colors: string | Array<string | undefined>,
|
|
1346
1353
|
options: { offset?: number; title?: string; force_overlay?: boolean; editable?: boolean }
|
|
1347
1354
|
): void {
|
|
1355
|
+
if (intrabarRun) return; // dropped: an intrabar run's outputs aren't read
|
|
1348
1356
|
const c = collector();
|
|
1349
1357
|
const id = `${kind}${c.barColorConfig.filter((x) => x.kind === kind).length}`;
|
|
1350
1358
|
c.barColorConfig.push({
|
|
@@ -1478,6 +1486,7 @@ export function plotcandle(
|
|
|
1478
1486
|
title?: string,
|
|
1479
1487
|
options: PlotCandleOptions = {}
|
|
1480
1488
|
): void {
|
|
1489
|
+
if (intrabarRun) return; // dropped: an intrabar run's outputs aren't read
|
|
1481
1490
|
const c = collector();
|
|
1482
1491
|
const color = orDefault(options.color, DEFAULT_CANDLE_COLORS.color);
|
|
1483
1492
|
const wickcolor = orDefault(options.wickcolor, DEFAULT_CANDLE_COLORS.wickColor);
|
|
@@ -1523,6 +1532,7 @@ export function plotbar(
|
|
|
1523
1532
|
title?: string,
|
|
1524
1533
|
options: PlotBarOptions = {}
|
|
1525
1534
|
): void {
|
|
1535
|
+
if (intrabarRun) return; // dropped: an intrabar run's outputs aren't read
|
|
1526
1536
|
const c = collector();
|
|
1527
1537
|
const barColor = orDefault(options.color, DEFAULT_CANDLE_COLORS.color);
|
|
1528
1538
|
const id = `plotbar${c.candleConfig.filter((x) => x.kind === 'bar').length}`;
|
|
@@ -1800,11 +1810,18 @@ function strategyEachBar(fn: (c: BarContext) => number | boolean | void): Series
|
|
|
1800
1810
|
// Intrabar run: the bars before replay the main run's answers, the bar itself calls the engine (whose
|
|
1801
1811
|
// orders the host is filling: no processBar / processClose).
|
|
1802
1812
|
const varipState = varipRun;
|
|
1813
|
+
const replayLog: { answers: readonly unknown[]; next: number } = { answers: [], next: 0 };
|
|
1814
|
+
const replayed = replayEngine(replayLog);
|
|
1803
1815
|
return eachBar((c) => {
|
|
1804
1816
|
const replay = c.i < intrabar.bar;
|
|
1805
|
-
|
|
1817
|
+
if (replay) {
|
|
1818
|
+
replayLog.answers = intrabar.recorder.answers[c.i] ?? [];
|
|
1819
|
+
replayLog.next = 0;
|
|
1820
|
+
}
|
|
1821
|
+
run.engine = replay ? replayed : intrabar.engine;
|
|
1806
1822
|
varipState.mode = replay ? 'replay' : 'live';
|
|
1807
|
-
|
|
1823
|
+
const starts = replay ? intrabar.recorder.varipStarts[c.i] ?? intrabar.recorder.varip : null;
|
|
1824
|
+
varipState.scratch = starts ? (starts.length ? [...starts] : NO_VARIP) : null;
|
|
1808
1825
|
run.inLoop = true;
|
|
1809
1826
|
try {
|
|
1810
1827
|
return fn(c);
|
|
@@ -1850,9 +1867,8 @@ function recordingEngine(engine: StrategyEngine, log: unknown[]): StrategyEngine
|
|
|
1850
1867
|
}
|
|
1851
1868
|
|
|
1852
1869
|
/** An engine that gives back recorded answers in order and drops the order calls (a replayed bar). */
|
|
1853
|
-
function replayEngine(log: readonly unknown[]): StrategyEngine {
|
|
1854
|
-
|
|
1855
|
-
const next = <T>(): T => log[k++] as T;
|
|
1870
|
+
function replayEngine(log: { answers: readonly unknown[]; next: number }): StrategyEngine {
|
|
1871
|
+
const next = <T>(): T => log.answers[log.next++] as T;
|
|
1856
1872
|
const none = (): void => {};
|
|
1857
1873
|
return {
|
|
1858
1874
|
processBar: none,
|
|
@@ -2477,6 +2493,27 @@ function barFlag(test: (i: number, n: number) => boolean): Series {
|
|
|
2477
2493
|
return Series.fromArray(ctxBars, Array.from({ length: n }, (_, i) => (test(i, n) ? 1 : 0)));
|
|
2478
2494
|
}
|
|
2479
2495
|
|
|
2496
|
+
/**
|
|
2497
|
+
* An intrabar run reads the flag on the same bars as the run before, with at most one bar more, and the flags only
|
|
2498
|
+
* differ on their last bars: the values are kept between runs (the series only reads them).
|
|
2499
|
+
*/
|
|
2500
|
+
const FLAG_TAIL = 3;
|
|
2501
|
+
const flagValues = new Map<string, number[]>();
|
|
2502
|
+
function namedFlag(name: string, test: (i: number, n: number) => boolean): Series {
|
|
2503
|
+
if (!intrabarRun) return barFlag(test);
|
|
2504
|
+
const n = ctxBars.bars.length;
|
|
2505
|
+
let values = flagValues.get(name);
|
|
2506
|
+
if (!values || values.length > n) {
|
|
2507
|
+
values = Array.from({ length: n }, (_, i) => (test(i, n) ? 1 : 0));
|
|
2508
|
+
flagValues.set(name, values);
|
|
2509
|
+
} else {
|
|
2510
|
+
const from = Math.max(0, Math.min(values.length, n) - FLAG_TAIL);
|
|
2511
|
+
values.length = n;
|
|
2512
|
+
for (let i = from; i < n; i++) values[i] = test(i, n) ? 1 : 0;
|
|
2513
|
+
}
|
|
2514
|
+
return Series.fromArray(ctxBars, values);
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2480
2517
|
/** Number of bars of the chart: an intrabar run ends before the chart's last bar. */
|
|
2481
2518
|
const chartBarCount = (n: number): number => (intrabarRun ? intrabarRun.lastBarIndex + 1 : n);
|
|
2482
2519
|
|
|
@@ -2490,14 +2527,14 @@ const lastRealtime = (i: number, n: number): boolean => i === n - 1 && chartCtx.
|
|
|
2490
2527
|
* `islastconfirmedhistory` is the last bar when it is confirmed and not realtime, else the bar before it.
|
|
2491
2528
|
*/
|
|
2492
2529
|
export const barstate = {
|
|
2493
|
-
get isfirst(): Series { return
|
|
2494
|
-
get islast(): Series { return
|
|
2495
|
-
get isconfirmed(): Series { return
|
|
2496
|
-
get isrealtime(): Series { return
|
|
2497
|
-
get ishistory(): Series { return
|
|
2498
|
-
get isnew(): Series { return
|
|
2530
|
+
get isfirst(): Series { return namedFlag('isfirst', (i) => i === 0); },
|
|
2531
|
+
get islast(): Series { return namedFlag('islast', (i, n) => i === chartBarCount(n) - 1); },
|
|
2532
|
+
get isconfirmed(): Series { return namedFlag('isconfirmed', (i, n) => i < n - 1 || chartCtx.lastBarConfirmed === true); },
|
|
2533
|
+
get isrealtime(): Series { return namedFlag('isrealtime', lastRealtime); },
|
|
2534
|
+
get ishistory(): Series { return namedFlag('ishistory', (i, n) => !lastRealtime(i, n)); },
|
|
2535
|
+
get isnew(): Series { return namedFlag('isnew', (i, n) => !lastRealtime(i, n) || chartCtx.lastBarNew !== false); },
|
|
2499
2536
|
get islastconfirmedhistory(): Series {
|
|
2500
|
-
return
|
|
2537
|
+
return namedFlag('islastconfirmedhistory', (i, n) => {
|
|
2501
2538
|
const lastConfirmed = chartCtx.lastBarConfirmed === true && chartCtx.realtime !== true;
|
|
2502
2539
|
const count = chartBarCount(n);
|
|
2503
2540
|
return i === (lastConfirmed ? count - 1 : count - 2);
|