oakscriptjs 0.10.1 → 0.11.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/dist/cjs/box/index.d.ts.map +1 -1
- package/dist/cjs/box/index.js +7 -1
- package/dist/cjs/box/index.js.map +2 -2
- package/dist/cjs/callsite/index.d.ts +73 -32
- package/dist/cjs/callsite/index.d.ts.map +1 -1
- package/dist/cjs/callsite/index.js +69 -65
- package/dist/cjs/callsite/index.js.map +2 -2
- package/dist/cjs/drawing/registry.d.ts +75 -0
- package/dist/cjs/drawing/registry.d.ts.map +1 -1
- package/dist/cjs/drawing/registry.js +139 -6
- package/dist/cjs/drawing/registry.js.map +2 -2
- package/dist/cjs/index.d.ts +6 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +28 -1
- package/dist/cjs/index.js.map +2 -2
- package/dist/cjs/label/index.d.ts.map +1 -1
- package/dist/cjs/label/index.js +3 -0
- package/dist/cjs/label/index.js.map +2 -2
- package/dist/cjs/line/index.d.ts.map +1 -1
- package/dist/cjs/line/index.js +7 -1
- package/dist/cjs/line/index.js.map +2 -2
- package/dist/cjs/math/index.d.ts.map +1 -1
- package/dist/cjs/math/index.js +2 -0
- package/dist/cjs/math/index.js.map +2 -2
- package/dist/cjs/polyline/index.d.ts.map +1 -1
- package/dist/cjs/polyline/index.js +3 -0
- package/dist/cjs/polyline/index.js.map +2 -2
- package/dist/cjs/script/index.d.ts +30 -13
- package/dist/cjs/script/index.d.ts.map +1 -1
- package/dist/cjs/script/index.js +48 -9
- package/dist/cjs/script/index.js.map +3 -3
- package/dist/cjs/session/bars.d.ts +15 -1
- package/dist/cjs/session/bars.d.ts.map +1 -1
- package/dist/cjs/session/bars.js +41 -6
- package/dist/cjs/session/bars.js.map +3 -3
- package/dist/cjs/session/calendar.d.ts +9 -1
- package/dist/cjs/session/calendar.d.ts.map +1 -1
- package/dist/cjs/session/calendar.js +28 -2
- package/dist/cjs/session/calendar.js.map +2 -2
- package/dist/cjs/session/context.d.ts +75 -0
- package/dist/cjs/session/context.d.ts.map +1 -0
- package/dist/cjs/session/context.js +113 -0
- package/dist/cjs/session/context.js.map +7 -0
- package/dist/cjs/str/index.d.ts +3 -1
- package/dist/cjs/str/index.d.ts.map +1 -1
- package/dist/cjs/str/index.js +2 -1
- package/dist/cjs/str/index.js.map +2 -2
- package/dist/cjs/str/numberformat.d.ts +4 -2
- package/dist/cjs/str/numberformat.d.ts.map +1 -1
- package/dist/cjs/str/numberformat.js +1 -1
- package/dist/cjs/str/numberformat.js.map +2 -2
- package/dist/cjs/ta/index.d.ts +33 -17
- package/dist/cjs/ta/index.d.ts.map +1 -1
- package/dist/cjs/ta/index.js +70 -29
- package/dist/cjs/ta/index.js.map +2 -2
- package/dist/cjs/ta/limits.d.ts +24 -0
- package/dist/cjs/ta/limits.d.ts.map +1 -0
- package/dist/cjs/ta/limits.js +50 -0
- package/dist/cjs/ta/limits.js.map +7 -0
- package/dist/cjs/ta/running-sum.d.ts +0 -16
- package/dist/cjs/ta/running-sum.d.ts.map +1 -1
- package/dist/cjs/ta/running-sum.js +2 -0
- package/dist/cjs/ta/running-sum.js.map +2 -2
- package/dist/cjs/ta-series.d.ts +7 -7
- package/dist/cjs/ta-series.d.ts.map +1 -1
- package/dist/cjs/ta-series.js +9 -7
- package/dist/cjs/ta-series.js.map +2 -2
- package/dist/cjs/time/index.d.ts +5 -2
- package/dist/cjs/time/index.d.ts.map +1 -1
- package/dist/cjs/time/index.js +2 -2
- package/dist/cjs/time/index.js.map +2 -2
- package/dist/cjs/time/session.d.ts +17 -1
- package/dist/cjs/time/session.d.ts.map +1 -1
- package/dist/cjs/time/session.js +61 -7
- package/dist/cjs/time/session.js.map +2 -2
- package/dist/cjs/types/metadata.d.ts +27 -3
- package/dist/cjs/types/metadata.d.ts.map +1 -1
- package/dist/cjs/types/metadata.js.map +1 -1
- package/dist/esm/box/index.d.ts.map +1 -1
- package/dist/esm/box/index.js +8 -2
- package/dist/esm/box/index.js.map +1 -1
- package/dist/esm/callsite/index.d.ts +73 -32
- package/dist/esm/callsite/index.d.ts.map +1 -1
- package/dist/esm/callsite/index.js +143 -106
- package/dist/esm/callsite/index.js.map +1 -1
- package/dist/esm/drawing/registry.d.ts +75 -0
- package/dist/esm/drawing/registry.d.ts.map +1 -1
- package/dist/esm/drawing/registry.js +237 -6
- package/dist/esm/drawing/registry.js.map +1 -1
- package/dist/esm/index.d.ts +6 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +10 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/label/index.d.ts.map +1 -1
- package/dist/esm/label/index.js +4 -1
- package/dist/esm/label/index.js.map +1 -1
- package/dist/esm/line/index.d.ts.map +1 -1
- package/dist/esm/line/index.js +8 -2
- package/dist/esm/line/index.js.map +1 -1
- package/dist/esm/math/index.d.ts.map +1 -1
- package/dist/esm/math/index.js +3 -0
- package/dist/esm/math/index.js.map +1 -1
- package/dist/esm/polyline/index.d.ts.map +1 -1
- package/dist/esm/polyline/index.js +5 -1
- package/dist/esm/polyline/index.js.map +1 -1
- package/dist/esm/script/index.d.ts +30 -13
- package/dist/esm/script/index.d.ts.map +1 -1
- package/dist/esm/script/index.js +54 -11
- package/dist/esm/script/index.js.map +1 -1
- package/dist/esm/session/bars.d.ts +15 -1
- package/dist/esm/session/bars.d.ts.map +1 -1
- package/dist/esm/session/bars.js +50 -6
- package/dist/esm/session/bars.js.map +1 -1
- package/dist/esm/session/calendar.d.ts +9 -1
- package/dist/esm/session/calendar.d.ts.map +1 -1
- package/dist/esm/session/calendar.js +35 -3
- package/dist/esm/session/calendar.js.map +1 -1
- package/dist/esm/session/context.d.ts +75 -0
- package/dist/esm/session/context.d.ts.map +1 -0
- package/dist/esm/session/context.js +122 -0
- package/dist/esm/session/context.js.map +1 -0
- package/dist/esm/str/index.d.ts +3 -1
- package/dist/esm/str/index.d.ts.map +1 -1
- package/dist/esm/str/index.js +8 -2
- package/dist/esm/str/index.js.map +1 -1
- package/dist/esm/str/numberformat.d.ts +4 -2
- package/dist/esm/str/numberformat.d.ts.map +1 -1
- package/dist/esm/str/numberformat.js +4 -2
- package/dist/esm/str/numberformat.js.map +1 -1
- package/dist/esm/ta/index.d.ts +33 -17
- package/dist/esm/ta/index.d.ts.map +1 -1
- package/dist/esm/ta/index.js +130 -56
- package/dist/esm/ta/index.js.map +1 -1
- package/dist/esm/ta/limits.d.ts +24 -0
- package/dist/esm/ta/limits.d.ts.map +1 -0
- package/dist/esm/ta/limits.js +42 -0
- package/dist/esm/ta/limits.js.map +1 -0
- package/dist/esm/ta/running-sum.d.ts +0 -16
- package/dist/esm/ta/running-sum.d.ts.map +1 -1
- package/dist/esm/ta/running-sum.js +2 -0
- package/dist/esm/ta/running-sum.js.map +1 -1
- package/dist/esm/ta-series.d.ts +7 -7
- package/dist/esm/ta-series.d.ts.map +1 -1
- package/dist/esm/ta-series.js +9 -7
- package/dist/esm/ta-series.js.map +1 -1
- package/dist/esm/time/index.d.ts +5 -2
- package/dist/esm/time/index.d.ts.map +1 -1
- package/dist/esm/time/index.js +6 -3
- package/dist/esm/time/index.js.map +1 -1
- package/dist/esm/time/session.d.ts +17 -1
- package/dist/esm/time/session.d.ts.map +1 -1
- package/dist/esm/time/session.js +97 -10
- package/dist/esm/time/session.js.map +1 -1
- package/dist/esm/types/metadata.d.ts +27 -3
- package/dist/esm/types/metadata.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/box/index.ts +8 -2
- package/src/callsite/index.ts +150 -100
- package/src/drawing/registry.ts +255 -5
- package/src/index.ts +34 -1
- package/src/label/index.ts +4 -1
- package/src/line/index.ts +8 -2
- package/src/math/index.ts +2 -0
- package/src/polyline/index.ts +3 -1
- package/src/script/index.ts +77 -18
- package/src/session/bars.ts +47 -6
- package/src/session/calendar.ts +37 -3
- package/src/session/context.ts +160 -0
- package/src/str/index.ts +7 -2
- package/src/str/numberformat.ts +5 -3
- package/src/ta/index.ts +148 -66
- package/src/ta/limits.ts +47 -0
- package/src/ta/running-sum.ts +2 -0
- package/src/ta-series.ts +16 -14
- package/src/time/index.ts +6 -3
- package/src/time/session.ts +89 -9
- package/src/types/metadata.ts +27 -3
package/src/script/index.ts
CHANGED
|
@@ -87,7 +87,8 @@ import * as tableCore from '../table/index.js';
|
|
|
87
87
|
import * as chartPointCore from '../chartpoint/index.js';
|
|
88
88
|
import * as textCore from '../text/index.js';
|
|
89
89
|
import * as mathCore from '../math/index.js';
|
|
90
|
-
import { resetDrawings, setDrawingLimits } from '../drawing/registry.js';
|
|
90
|
+
import { resetDrawings, setDrawingBar, setDrawingLimits } from '../drawing/registry.js';
|
|
91
|
+
export { holdDrawing, releaseDrawing, varDrawing, type DrawingVariable } from '../drawing/registry.js';
|
|
91
92
|
import { drawingOutputs } from '../drawing/output.js';
|
|
92
93
|
import * as timeframeCore from '../timeframe/index.js';
|
|
93
94
|
import * as timeCore from '../time/index.js';
|
|
@@ -296,6 +297,7 @@ export function executeScript(
|
|
|
296
297
|
): ScriptRunResult {
|
|
297
298
|
ctxBars.setAll(barsInput);
|
|
298
299
|
chartCtx = { ...chart };
|
|
300
|
+
runNow = chart.now ?? Date.now();
|
|
299
301
|
scriptOptions = options;
|
|
300
302
|
ctx = freshCollector(inputs);
|
|
301
303
|
resetDrawings();
|
|
@@ -547,25 +549,27 @@ export const ta = {
|
|
|
547
549
|
): [Series, Series, Series, Series, Series] =>
|
|
548
550
|
taSeries.ichimoku(bars(), conversionPeriods, basePeriods, laggingSpan2Periods, displacement),
|
|
549
551
|
/** `ta.highest(source, length)` or `ta.highest(length)` (source = high). */
|
|
550
|
-
highest: (sourceOrLength: Series | number, length?: number): Series =>
|
|
552
|
+
highest: (sourceOrLength: Series | number, length?: number | Series): Series =>
|
|
551
553
|
typeof sourceOrLength === 'number' ? taSeries.highest(high, sourceOrLength) : taSeries.highest(sourceOrLength, length!),
|
|
552
554
|
/** `ta.lowest(source, length)` or `ta.lowest(length)` (source = low). */
|
|
553
|
-
lowest: (sourceOrLength: Series | number, length?: number): Series =>
|
|
555
|
+
lowest: (sourceOrLength: Series | number, length?: number | Series): Series =>
|
|
554
556
|
typeof sourceOrLength === 'number' ? taSeries.lowest(low, sourceOrLength) : taSeries.lowest(sourceOrLength, length!),
|
|
555
557
|
/** `ta.highestbars(source, length)` or `ta.highestbars(length)` (source = high); offsets are 0 or negative. */
|
|
556
|
-
highestbars: (sourceOrLength: Series | number, length?: number): Series =>
|
|
558
|
+
highestbars: (sourceOrLength: Series | number, length?: number | Series): Series =>
|
|
557
559
|
typeof sourceOrLength === 'number'
|
|
558
560
|
? taSeries.highestbars(high, sourceOrLength)
|
|
559
561
|
: taSeries.highestbars(sourceOrLength, length!),
|
|
560
562
|
/** `ta.lowestbars(source, length)` or `ta.lowestbars(length)` (source = low); offsets are 0 or negative. */
|
|
561
|
-
lowestbars: (sourceOrLength: Series | number, length?: number): Series =>
|
|
563
|
+
lowestbars: (sourceOrLength: Series | number, length?: number | Series): Series =>
|
|
562
564
|
typeof sourceOrLength === 'number'
|
|
563
565
|
? taSeries.lowestbars(low, sourceOrLength)
|
|
564
566
|
: taSeries.lowestbars(sourceOrLength, length!),
|
|
565
567
|
/** `ta.vwap(source, anchor?, stdev_mult?)` with the chart volume; the default anchor is a new trading
|
|
566
|
-
* day (`timeframe.change("1D")`), which needs the chart context (timeframe, time zone, session)
|
|
568
|
+
* day (`timeframe.change("1D")`), which needs the chart context (timeframe, time zone, session), and the
|
|
569
|
+
* first bar: as in PineScript, the first loaded bars (a day in progress) have a value. */
|
|
567
570
|
vwap: ((source: Series, anchor?: Series, stdev_mult?: number): Series | [Series, Series, Series] => {
|
|
568
|
-
|
|
571
|
+
// PineScript default: timeframe.change("1D"); the sums also start on the first bar (a day in progress)
|
|
572
|
+
const a = anchor ?? changeOf('1D', true);
|
|
569
573
|
return stdev_mult === undefined ? taSeries.vwap(source, volume, a) : taSeries.vwapBands(source, volume, a, stdev_mult);
|
|
570
574
|
}) as {
|
|
571
575
|
(source: Series, anchor?: Series): Series;
|
|
@@ -575,12 +579,12 @@ export const ta = {
|
|
|
575
579
|
mfi: (source: Series, length: number): Series => taSeries.mfi(source, length, volume),
|
|
576
580
|
/** `ta.pivothigh(source, leftbars, rightbars)` or `ta.pivothigh(leftbars, rightbars)` (source = high).
|
|
577
581
|
* The value appears `rightbars` bars after the pivot bar, as in PineScript. */
|
|
578
|
-
pivothigh: (sourceOrLeft: Series | number, leftOrRight: number, rightbars?: number): Series =>
|
|
582
|
+
pivothigh: (sourceOrLeft: Series | number, leftOrRight: number | Series, rightbars?: number | Series): Series =>
|
|
579
583
|
typeof sourceOrLeft === 'number'
|
|
580
584
|
? taSeries.pivothigh(high, sourceOrLeft, leftOrRight)
|
|
581
585
|
: taSeries.pivothigh(sourceOrLeft, leftOrRight, rightbars!),
|
|
582
586
|
/** `ta.pivotlow(source, leftbars, rightbars)` or `ta.pivotlow(leftbars, rightbars)` (source = low). */
|
|
583
|
-
pivotlow: (sourceOrLeft: Series | number, leftOrRight: number, rightbars?: number): Series =>
|
|
587
|
+
pivotlow: (sourceOrLeft: Series | number, leftOrRight: number | Series, rightbars?: number | Series): Series =>
|
|
584
588
|
typeof sourceOrLeft === 'number'
|
|
585
589
|
? taSeries.pivotlow(low, sourceOrLeft, leftOrRight)
|
|
586
590
|
: taSeries.pivotlow(sourceOrLeft, leftOrRight, rightbars!),
|
|
@@ -1262,6 +1266,7 @@ export function eachBar(fn: (c: BarContext) => number | boolean | void): Series
|
|
|
1262
1266
|
for (idx = 0; idx < b.length; idx++) {
|
|
1263
1267
|
if (pendingError && pendingError.bar === idx) throw new Error(historyError(idx, pendingError.barsBack));
|
|
1264
1268
|
currentBar = idx;
|
|
1269
|
+
setDrawingBar(idx, idx === b.length - 1);
|
|
1265
1270
|
const r = fn(ctx);
|
|
1266
1271
|
out[idx] = typeof r === 'number' ? r : typeof r === 'boolean' ? (r ? 1 : 0) : NaN;
|
|
1267
1272
|
// grow the history buffers from the largest reference of this bar
|
|
@@ -1278,6 +1283,7 @@ export function eachBar(fn: (c: BarContext) => number | boolean | void): Series
|
|
|
1278
1283
|
}
|
|
1279
1284
|
} finally {
|
|
1280
1285
|
currentBar = -1;
|
|
1286
|
+
setDrawingBar(null);
|
|
1281
1287
|
}
|
|
1282
1288
|
return Series.fromArray(ctxBars, out);
|
|
1283
1289
|
}
|
|
@@ -1547,6 +1553,16 @@ export interface ChartContext {
|
|
|
1547
1553
|
pointvalue?: number;
|
|
1548
1554
|
/** Smallest tradable quantity (PineScript `syminfo.mincontract`); 1 for whole shares */
|
|
1549
1555
|
mincontract?: number;
|
|
1556
|
+
/** Symbol type (PineScript `syminfo.type`): "stock", "crypto", "forex", "futures", "index", "fund"... */
|
|
1557
|
+
symbolType?: string;
|
|
1558
|
+
/** Currency of the symbol (PineScript `syminfo.currency`), e.g. "USD" */
|
|
1559
|
+
currency?: string;
|
|
1560
|
+
/** Chart foreground colour (PineScript `chart.fg_color`), e.g. "#DBDBDB" on a dark chart */
|
|
1561
|
+
foregroundColor?: string;
|
|
1562
|
+
/** Chart background colour (PineScript `chart.bg_color`), e.g. "#0F0F0F" on a dark chart */
|
|
1563
|
+
backgroundColor?: string;
|
|
1564
|
+
/** Current time in ms (PineScript `timenow`); default: the clock of the computer at the start of the run */
|
|
1565
|
+
now?: number;
|
|
1550
1566
|
/**
|
|
1551
1567
|
* The last bar is closed (`barstate.isconfirmed` on the last bar). Default false: PineScript treats the last bar
|
|
1552
1568
|
* of the data as not confirmed until the data feed closes it.
|
|
@@ -1638,6 +1654,16 @@ function symbolProperty(key: 'mintick' | 'pointvalue' | 'mincontract'): number {
|
|
|
1638
1654
|
return value;
|
|
1639
1655
|
}
|
|
1640
1656
|
|
|
1657
|
+
/** A text value of the chart or the symbol that the host passes in the chart context. */
|
|
1658
|
+
function chartText(key: 'symbolType' | 'currency' | 'foregroundColor' | 'backgroundColor', name: string): string {
|
|
1659
|
+
collector();
|
|
1660
|
+
const value = chartCtx[key];
|
|
1661
|
+
if (value === undefined) {
|
|
1662
|
+
throw new Error(`${name} is not known: pass it as executeScript(body, bars, inputs, { ${key} }).`);
|
|
1663
|
+
}
|
|
1664
|
+
return value;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1641
1667
|
/** `[minmove, pricescale]` of the chart symbol: from the chart context, else from its mintick. */
|
|
1642
1668
|
function symbolScale(): [number, number] {
|
|
1643
1669
|
collector();
|
|
@@ -1649,7 +1675,8 @@ function symbolScale(): [number, number] {
|
|
|
1649
1675
|
|
|
1650
1676
|
/**
|
|
1651
1677
|
* PineScript `syminfo.timezone`, `syminfo.tickerid`, `syminfo.session` (session type), `syminfo.mintick`,
|
|
1652
|
-
* `syminfo.minmove`, `syminfo.pricescale`, `syminfo.pointvalue
|
|
1678
|
+
* `syminfo.minmove`, `syminfo.pricescale`, `syminfo.pointvalue`, `syminfo.mincontract`, `syminfo.type` and
|
|
1679
|
+
* `syminfo.currency`, from the chart context.
|
|
1653
1680
|
*/
|
|
1654
1681
|
export const syminfo = {
|
|
1655
1682
|
get mintick(): number {
|
|
@@ -1681,6 +1708,12 @@ export const syminfo = {
|
|
|
1681
1708
|
collector();
|
|
1682
1709
|
return chartCtx.sessionType ?? 'regular';
|
|
1683
1710
|
},
|
|
1711
|
+
get type(): string {
|
|
1712
|
+
return chartText('symbolType', 'syminfo.type');
|
|
1713
|
+
},
|
|
1714
|
+
get currency(): string {
|
|
1715
|
+
return chartText('currency', 'syminfo.currency');
|
|
1716
|
+
},
|
|
1684
1717
|
};
|
|
1685
1718
|
|
|
1686
1719
|
/** A calendar function: `(time, timezone?)` on a UNIX time or on a Series of times. */
|
|
@@ -1794,7 +1827,10 @@ function sessionBars(): { bars: SessionBars; regular: SessionBars } {
|
|
|
1794
1827
|
* session. As in PineScript, an explicit time zone reads the symbol session hours in that zone without
|
|
1795
1828
|
* corrections and holidays, and an explicit session follows PineScript's session string rules.
|
|
1796
1829
|
*/
|
|
1797
|
-
function calendarFor(
|
|
1830
|
+
function calendarFor(
|
|
1831
|
+
session?: string,
|
|
1832
|
+
timezone?: string
|
|
1833
|
+
): { bars: SessionBars; test: boolean; inside?: (time: number) => boolean } {
|
|
1798
1834
|
const { bars } = sessionBars();
|
|
1799
1835
|
if (!session && !timezone) return { bars, test: false };
|
|
1800
1836
|
const tz = timezone ?? exchangeTimezone();
|
|
@@ -1806,7 +1842,14 @@ function calendarFor(session?: string, timezone?: string): { bars: SessionBars;
|
|
|
1806
1842
|
return { bars: new SessionBars(new TradingCalendar(tz, { session: toSpec(symbol).session }, 'symbol'), bars.chart.period), test: true };
|
|
1807
1843
|
}
|
|
1808
1844
|
const spec = session ? { session } : { session: toSpec(chartCtx.session!).session };
|
|
1809
|
-
return {
|
|
1845
|
+
return {
|
|
1846
|
+
bars: new SessionBars(new TradingCalendar(tz, spec, session ? 'pine' : 'symbol'), bars.chart.period),
|
|
1847
|
+
test: true,
|
|
1848
|
+
// a PineScript session string: the rules of time.inSession (runs ending at 0000, days around a clock change)
|
|
1849
|
+
inside: session
|
|
1850
|
+
? (time) => timeCore.inSession(time, session, tz, timeframeCore.in_seconds(bars.chart.period) * 1000)
|
|
1851
|
+
: undefined,
|
|
1852
|
+
};
|
|
1810
1853
|
}
|
|
1811
1854
|
|
|
1812
1855
|
function perBar(fn: (t: number, i: number, times: number[]) => number): Series {
|
|
@@ -1823,22 +1866,28 @@ function callableSeries<F extends (...args: never[]) => Series>(series: Series,
|
|
|
1823
1866
|
|
|
1824
1867
|
function timeFunction(tf?: string, session?: string, timezone?: string): Series {
|
|
1825
1868
|
collector();
|
|
1826
|
-
const { bars, test } = calendarFor(session, timezone);
|
|
1869
|
+
const { bars, test, inside } = calendarFor(session, timezone);
|
|
1827
1870
|
const timeframeString = tf || chartTimeframe().period;
|
|
1828
|
-
return perBar((t) => (test && !bars.inside(t) ? NaN : bars.timeOf(t, timeframeString)));
|
|
1871
|
+
return perBar((t) => (test && !(inside ? inside(t) : bars.inside(t)) ? NaN : bars.timeOf(t, timeframeString)));
|
|
1829
1872
|
}
|
|
1830
1873
|
|
|
1831
1874
|
function timeCloseFunction(tf?: string, session?: string, timezone?: string): Series {
|
|
1832
1875
|
collector();
|
|
1833
|
-
const { bars, test } = calendarFor(session, timezone);
|
|
1876
|
+
const { bars, test, inside } = calendarFor(session, timezone);
|
|
1834
1877
|
const timeframeString = tf || chartTimeframe().period;
|
|
1835
|
-
return perBar((t) => (test && !bars.inside(t) ? NaN : bars.closeOf(t, timeframeString)));
|
|
1878
|
+
return perBar((t) => (test && !(inside ? inside(t) : bars.inside(t)) ? NaN : bars.closeOf(t, timeframeString)));
|
|
1836
1879
|
}
|
|
1837
1880
|
|
|
1838
1881
|
/**
|
|
1839
1882
|
* PineScript `time`: the bar open time in ms (a Series), and `time(timeframe?, session?, timezone?)`:
|
|
1840
1883
|
* the open time of the `timeframe` period that contains the bar, `na` outside `session`.
|
|
1841
1884
|
*/
|
|
1885
|
+
/** Current time of the run in ms, see `ChartContext.now`. */
|
|
1886
|
+
let runNow = NaN;
|
|
1887
|
+
|
|
1888
|
+
/** PineScript `timenow`: the current time in ms, the same on every bar of a run (`ChartContext.now`). */
|
|
1889
|
+
export const timenow = new Series(ctxBars, () => runNow);
|
|
1890
|
+
|
|
1842
1891
|
export const time = callableSeries(new Series(ctxBars, (b) => barTime(b)), timeFunction);
|
|
1843
1892
|
|
|
1844
1893
|
/**
|
|
@@ -1853,9 +1902,11 @@ export const time_close = callableSeries(
|
|
|
1853
1902
|
/** PineScript `time_tradingday`: 00:00 UTC of the trading day of each bar, in ms. */
|
|
1854
1903
|
export const time_tradingday = new Series(ctxBars, (b) => sessionBars().bars.tradingDayTime(barTime(b)));
|
|
1855
1904
|
|
|
1856
|
-
function changeOf(tf: string): Series {
|
|
1905
|
+
function changeOf(tf: string, firstBar = false): Series {
|
|
1857
1906
|
const { bars } = sessionBars();
|
|
1858
|
-
|
|
1907
|
+
// a bar without a period (after an early close) is no change, and neither is the bar after it
|
|
1908
|
+
const changed = (a: number, b: number): boolean => !Number.isNaN(a) && !Number.isNaN(b) && a !== b;
|
|
1909
|
+
return perBar((t, i, times) => ((i > 0 ? changed(bars.timeOf(t, tf), bars.timeOf(times[i - 1]!, tf)) : firstBar) ? 1 : 0));
|
|
1859
1910
|
}
|
|
1860
1911
|
|
|
1861
1912
|
function flag(key: keyof SessionFlags): Series {
|
|
@@ -2091,6 +2142,14 @@ export const text = { ...textCore };
|
|
|
2091
2142
|
* (index, time, and `price`, default the close), so it runs inside eachBar().
|
|
2092
2143
|
*/
|
|
2093
2144
|
export const chart = {
|
|
2145
|
+
/** Chart foreground colour (PineScript `chart.fg_color`), from the chart context. */
|
|
2146
|
+
get fg_color(): string {
|
|
2147
|
+
return chartText('foregroundColor', 'chart.fg_color');
|
|
2148
|
+
},
|
|
2149
|
+
/** Chart background colour (PineScript `chart.bg_color`), from the chart context. */
|
|
2150
|
+
get bg_color(): string {
|
|
2151
|
+
return chartText('backgroundColor', 'chart.bg_color');
|
|
2152
|
+
},
|
|
2094
2153
|
point: {
|
|
2095
2154
|
...chartPointCore,
|
|
2096
2155
|
now(price?: number): ChartPoint {
|
package/src/session/bars.ts
CHANGED
|
@@ -111,7 +111,10 @@ export class SessionBars {
|
|
|
111
111
|
return p[p.length - 1]![1];
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* PineScript `time(tf)` for a bar that opens at `time`; NaN when no trading day is found. An intraday bar that
|
|
116
|
+
* opens after an early close has no day: `time("D")` is NaN on it (measured; `time("W")` keeps its value).
|
|
117
|
+
*/
|
|
115
118
|
timeOf(time: number, timeframe: string): number {
|
|
116
119
|
const tf = info(timeframe);
|
|
117
120
|
const at = this.calendar.locate(time);
|
|
@@ -123,6 +126,7 @@ export class SessionBars {
|
|
|
123
126
|
return start + Math.floor((time - start) / ms) * ms;
|
|
124
127
|
}
|
|
125
128
|
const { unit, n } = unitOf(tf);
|
|
129
|
+
if (unit === 'D' && n === 1 && this.stray(time)) return NaN;
|
|
126
130
|
const [from, to] = this.periodRange(at.date, unit, n);
|
|
127
131
|
const first = this.calendar.firstTradingDay(from, to);
|
|
128
132
|
return first === null ? NaN : this.dayStart(first);
|
|
@@ -140,6 +144,7 @@ export class SessionBars {
|
|
|
140
144
|
return Math.min(this.timeOf(time, timeframe) + in_seconds(tf.period) * 1000, end);
|
|
141
145
|
}
|
|
142
146
|
const { unit, n } = unitOf(tf);
|
|
147
|
+
if (unit === 'D' && n === 1 && this.stray(time)) return NaN;
|
|
143
148
|
const [from, to] = this.periodRange(at.date, unit, n);
|
|
144
149
|
// intraday charts: the start of the next period, except "1D" (the session end)
|
|
145
150
|
if (this.chart.isintraday && (unit !== 'D' || n > 1)) {
|
|
@@ -154,7 +159,7 @@ export class SessionBars {
|
|
|
154
159
|
/** Trading day of a bar: on D / W / M charts the last trading day of the chart period. */
|
|
155
160
|
tradingDay(time: number): DateNum | null {
|
|
156
161
|
const at = this.calendar.locate(time);
|
|
157
|
-
if (!at) return null;
|
|
162
|
+
if (!at || this.stray(time)) return null;
|
|
158
163
|
if (this.chart.isdwm) {
|
|
159
164
|
const { unit, n } = unitOf(this.chart);
|
|
160
165
|
const [from, to] = this.periodRange(at.date, unit, n);
|
|
@@ -188,6 +193,35 @@ export class SessionBars {
|
|
|
188
193
|
return day === null ? NaN : dateUtcMs(day);
|
|
189
194
|
}
|
|
190
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Open time of the bar after the bar that opens at `time`, on the trading calendar: the next bar of the session
|
|
198
|
+
* period, else the start of the next period or trading day; on D / W / M charts the session start of the first
|
|
199
|
+
* trading day of the next chart period. NaN when none is found within 400 days.
|
|
200
|
+
*/
|
|
201
|
+
nextBarTime(time: number): number {
|
|
202
|
+
const at = this.calendar.locate(time);
|
|
203
|
+
if (!at) return NaN;
|
|
204
|
+
if (!this.chart.isdwm) {
|
|
205
|
+
const periods = this.calendar.periods(at.date);
|
|
206
|
+
const [start, end] = periods[at.index]!;
|
|
207
|
+
const ms = in_seconds(this.chart.period) * 1000;
|
|
208
|
+
const next = start + (Math.floor((time - start) / ms) + 1) * ms;
|
|
209
|
+
if (at.inside && next < end) return next;
|
|
210
|
+
if (at.index + 1 < periods.length) return periods[at.index + 1]![0];
|
|
211
|
+
const day = this.calendar.firstTradingDay(addDays(at.date, 1), addDays(at.date, 400));
|
|
212
|
+
return day === null ? NaN : this.dayStart(day);
|
|
213
|
+
}
|
|
214
|
+
const { unit, n } = unitOf(this.chart);
|
|
215
|
+
const [, to] = this.periodRange(at.date, unit, n);
|
|
216
|
+
const day = this.calendar.firstTradingDay(addDays(to, 1), addDays(to, 400));
|
|
217
|
+
return day === null ? NaN : this.dayStart(day);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** An intraday bar that opens after an early close of its day (see TradingCalendar.afterEarlyClose). */
|
|
221
|
+
stray(time: number): boolean {
|
|
222
|
+
return !this.chart.isdwm && this.calendar.afterEarlyClose(time);
|
|
223
|
+
}
|
|
224
|
+
|
|
191
225
|
/** True when the bar at `time` is inside the session. */
|
|
192
226
|
inside(time: number): boolean {
|
|
193
227
|
return this.calendar.locate(time)?.inside ?? false;
|
|
@@ -217,6 +251,9 @@ export interface SessionFlags {
|
|
|
217
251
|
* time; first and last bars are per session period (a trading day can have two periods, e.g. after a
|
|
218
252
|
* futures holiday); pre / post market compare with the regular hours of the same trading day; on
|
|
219
253
|
* D / W / M charts every bar is its own session (first, last, market, first and last regular).
|
|
254
|
+
* The first loaded bar is a first bar only when no earlier bar of its period can exist (it opens less than one chart
|
|
255
|
+
* period after the period start). A bar that opens after an early close is in the market hours (the usual hours of
|
|
256
|
+
* its day) but is no last bar.
|
|
220
257
|
*/
|
|
221
258
|
export function sessionFlags(times: number[], bars: SessionBars, regular: SessionBars): SessionFlags {
|
|
222
259
|
const n = times.length;
|
|
@@ -242,18 +279,22 @@ export function sessionFlags(times: number[], bars: SessionBars, regular: Sessio
|
|
|
242
279
|
const close = bars.closeOf(t, chart.period);
|
|
243
280
|
const [, end] = bars.calendar.periods(at.date)[at.index]!;
|
|
244
281
|
const period = `${at.date}/${at.index}`;
|
|
245
|
-
|
|
246
|
-
|
|
282
|
+
const [start] = bars.calendar.periods(at.date)[at.index]!;
|
|
283
|
+
const barMs = in_seconds(chart.period) * 1000;
|
|
284
|
+
// the first loaded bar: a first bar only when no earlier bar of the period can exist
|
|
285
|
+
out.isfirstbar[i] = i === 0 ? t - start < barMs : period !== previousPeriod;
|
|
286
|
+
const stray = bars.stray(t);
|
|
287
|
+
out.islastbar[i] = !stray && close >= end;
|
|
247
288
|
const regularPeriods = regular.calendar.periods(at.date);
|
|
248
289
|
const r = regularPeriods.findIndex(([s, e]) => s <= t && t < e);
|
|
249
290
|
const market = r !== -1;
|
|
250
|
-
out.ismarket[i] = market;
|
|
291
|
+
out.ismarket[i] = market || regular.stray(t);
|
|
251
292
|
if (!market && regularPeriods.length) {
|
|
252
293
|
out.ispremarket[i] = t < regularPeriods[0]![0];
|
|
253
294
|
out.ispostmarket[i] = t >= regularPeriods[regularPeriods.length - 1]![1];
|
|
254
295
|
}
|
|
255
296
|
const regularPeriod = market ? `${at.date}/${r}` : '';
|
|
256
|
-
out.isfirstbar_regular[i] = market && regularPeriod !== previousRegular;
|
|
297
|
+
out.isfirstbar_regular[i] = market && (i === 0 ? t - regularPeriods[r]![0] < barMs : regularPeriod !== previousRegular);
|
|
257
298
|
out.islastbar_regular[i] = market && close >= regularPeriods[r]![1];
|
|
258
299
|
previousPeriod = period;
|
|
259
300
|
previousRegular = regularPeriod;
|
package/src/session/calendar.ts
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* - periods "HHMM-HHMM", several separated by commas; "24x7" is the whole day, every day
|
|
7
7
|
* - a period whose end is not after its start ("1700-1600", "1700-1700") starts the day before the
|
|
8
8
|
* trading day: the session that opens Sunday 17:00 is Monday's
|
|
9
|
-
* - "HHMMF<n>" moves a time <n> days before the trading day ("1700F2-1200F1")
|
|
9
|
+
* - "HHMMF<n>" moves a time <n> days before the trading day ("1700F2-1200F1"); "F" alone is one day ("1700F-1900":
|
|
10
|
+
* 17:00 of the day before to 19:00)
|
|
11
|
+
* - several sections joined by "|", each with its own days ("1900-1900:3456|1700F-1900:2": Monday opens on Sunday
|
|
12
|
+
* 17:00, the other days at 19:00 of the day before)
|
|
10
13
|
* - optional days after ":" (1 = Sunday ... 7 = Saturday); without days a symbol session trades Monday to
|
|
11
14
|
* Friday ("24x7": every day), and a PineScript session string follows PineScript's rules (one period:
|
|
12
15
|
* every day; several periods: Monday to Friday)
|
|
@@ -62,9 +65,10 @@ export const weekdayOf = (date: DateNum): number => new Date(utcOf(date)).getUTC
|
|
|
62
65
|
export const dateUtcMs = utcOf;
|
|
63
66
|
|
|
64
67
|
function parseTime(token: string, spec: string): { minutes: number; offset?: number } {
|
|
65
|
-
const m = /^(\d{2})(\d{2})(?:F(\d
|
|
68
|
+
const m = /^(\d{2})(\d{2})(?:F(\d*))?$/.exec(token.trim());
|
|
66
69
|
if (!m) throw new SyntaxError(`Invalid session "${spec}"`);
|
|
67
|
-
|
|
70
|
+
// "F" without a number is one day before the trading day
|
|
71
|
+
return { minutes: Number(m[1]) * 60 + Number(m[2]), offset: m[3] === undefined ? undefined : Number(m[3] || 1) };
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
function parsePeriods(text: string, spec: string): Period[] {
|
|
@@ -102,6 +106,17 @@ function pineVersion(text: string, from: DateNum): Version {
|
|
|
102
106
|
|
|
103
107
|
function parseVersion(text: string, from: DateNum, spec: string, rule: 'symbol' | 'pine'): Version {
|
|
104
108
|
if (rule === 'pine') return pineVersion(text, from);
|
|
109
|
+
if (text.includes('|')) {
|
|
110
|
+
// sections with their own days
|
|
111
|
+
const byDay: Period[][] = Array.from({ length: 8 }, () => []);
|
|
112
|
+
for (const section of text.split('|').filter((part) => part.trim())) {
|
|
113
|
+
const one = parseVersion(section, from, spec, rule);
|
|
114
|
+
one.days.forEach((on, d) => {
|
|
115
|
+
if (on) byDay[d] = one.periods;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return { from, periods: [], days: byDay.map((p) => p.length > 0), byDay };
|
|
119
|
+
}
|
|
105
120
|
const [periodText = '', dayText] = text.trim().split(':');
|
|
106
121
|
const periods = parsePeriods(periodText, spec);
|
|
107
122
|
const allDays = periodText.trim() === '24x7';
|
|
@@ -174,6 +189,25 @@ export class TradingCalendar {
|
|
|
174
189
|
return this.periods(date).length > 0;
|
|
175
190
|
}
|
|
176
191
|
|
|
192
|
+
/**
|
|
193
|
+
* True for a bar that opens after the session of its day was cut by a correction (an early close) but inside the
|
|
194
|
+
* usual hours of that day: PineScript has no period for such a bar (`time("D")` is na on it).
|
|
195
|
+
*/
|
|
196
|
+
afterEarlyClose(time: number): boolean {
|
|
197
|
+
const at = this.locate(time);
|
|
198
|
+
if (!at || at.inside || !this.corrections.get(at.date)) return false;
|
|
199
|
+
const version = [...this.versions].reverse().find((v) => v.from <= at.date)!;
|
|
200
|
+
const usual = version.byDay ? version.byDay[weekdayOf(at.date)]! : version.days[weekdayOf(at.date)] ? version.periods : [];
|
|
201
|
+
const y = Math.floor(at.date / 10000);
|
|
202
|
+
const m = Math.floor(at.date / 100) % 100;
|
|
203
|
+
const d = at.date % 100;
|
|
204
|
+
return usual.some(
|
|
205
|
+
(p) =>
|
|
206
|
+
zonedToUnix(this.timezone, y, m, d - p.startOffset, Math.floor(p.start / 60), p.start % 60) <= time &&
|
|
207
|
+
time < zonedToUnix(this.timezone, y, m, d - p.endOffset, Math.floor(p.end / 60), p.end % 60)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
177
211
|
/**
|
|
178
212
|
* Trading day and period of a time: the period that contains it, else the last period that started
|
|
179
213
|
* before it. Null when no trading day is found near the time.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session of a chart context, for code that does not run a whole script (a port with its own bar loop).
|
|
3
|
+
*
|
|
4
|
+
* The same values as the script API gives (`time(tf)`, `time_close(tf)`, `time_tradingday`, `session.*`, the periods
|
|
5
|
+
* of `request.security`), from the session, corrections and holidays of the context. Bar times are in the unit of
|
|
6
|
+
* the context (`timeUnit`, seconds by default, as `Bar.time`); PineScript time values are returned in ms.
|
|
7
|
+
*
|
|
8
|
+
* @module session/context
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Bar } from '../types/index.js';
|
|
12
|
+
import { info, in_seconds } from '../timeframe/index.js';
|
|
13
|
+
import { afterCompletion, periodsOf, resample } from '../security/resample.js';
|
|
14
|
+
import { SessionBars, sessionFlags, type SessionFlags } from './bars.js';
|
|
15
|
+
import { TradingCalendar, type SessionSpec } from './calendar.js';
|
|
16
|
+
|
|
17
|
+
/** The part of a chart context that the session functions read (a `ChartContext` of the script API fits). */
|
|
18
|
+
export interface SessionContext {
|
|
19
|
+
/** Chart timeframe: "5", "60", "1D"... */
|
|
20
|
+
timeframe?: string;
|
|
21
|
+
/** Exchange time zone: "America/New_York", "Etc/UTC"... */
|
|
22
|
+
timezone?: string;
|
|
23
|
+
/** Session of the loaded bars: hours, corrections, holidays */
|
|
24
|
+
session?: SessionSpec | string;
|
|
25
|
+
/** Regular trading hours when the bars include extended hours (default: `session`) */
|
|
26
|
+
regularSession?: SessionSpec | string;
|
|
27
|
+
/** Unit of the bar times: 's' (default) or 'ms' */
|
|
28
|
+
timeUnit?: 's' | 'ms';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The session calendars of a context: the loaded bars and the regular hours. */
|
|
32
|
+
export interface ChartSessions {
|
|
33
|
+
bars: SessionBars;
|
|
34
|
+
regular: SessionBars;
|
|
35
|
+
/** ms per unit of the bar times */
|
|
36
|
+
unit: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const toSpec = (s: SessionSpec | string): SessionSpec => (typeof s === 'string' ? { session: s } : s);
|
|
40
|
+
const cache = new WeakMap<object, ChartSessions>();
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Session calendars of a chart context (cached per context object).
|
|
44
|
+
* @throws Error when the context has no timeframe, time zone or session
|
|
45
|
+
*/
|
|
46
|
+
export function chartSessions(chart: SessionContext): ChartSessions {
|
|
47
|
+
const cached = cache.get(chart);
|
|
48
|
+
if (cached) return cached;
|
|
49
|
+
const { timeframe, timezone, session } = chart;
|
|
50
|
+
if (!timeframe || !timezone || !session) {
|
|
51
|
+
throw new Error('The chart context needs timeframe, timezone and session for the session functions.');
|
|
52
|
+
}
|
|
53
|
+
const loaded = toSpec(session);
|
|
54
|
+
const regular = chart.regularSession ? toSpec(chart.regularSession) : loaded;
|
|
55
|
+
const out: ChartSessions = {
|
|
56
|
+
bars: new SessionBars(new TradingCalendar(timezone, loaded), timeframe),
|
|
57
|
+
regular: new SessionBars(new TradingCalendar(timezone, { holidays: loaded.holidays, ...regular }), timeframe),
|
|
58
|
+
unit: chart.timeUnit === 'ms' ? 1 : 1000,
|
|
59
|
+
};
|
|
60
|
+
cache.set(chart, out);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** PineScript `time(timeframe)` (ms) of a bar that opens at `time`: the start of its period; NaN without one. */
|
|
65
|
+
export function periodTime(time: number, timeframe: string, chart: SessionContext): number {
|
|
66
|
+
const s = chartSessions(chart);
|
|
67
|
+
return s.bars.timeOf(time * s.unit, timeframe);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** PineScript `time_close(timeframe)` (ms) of a bar that opens at `time`. */
|
|
71
|
+
export function periodClose(time: number, timeframe: string, chart: SessionContext): number {
|
|
72
|
+
const s = chartSessions(chart);
|
|
73
|
+
return s.bars.closeOf(time * s.unit, timeframe);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** PineScript `time_tradingday` (ms, 00:00 UTC of the trading day) of a bar that opens at `time`. */
|
|
77
|
+
export function tradingDayTime(time: number, chart: SessionContext): number {
|
|
78
|
+
const s = chartSessions(chart);
|
|
79
|
+
return s.bars.tradingDayTime(time * s.unit);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** PineScript `session.*` flags of bars that open at `times` (in order). */
|
|
83
|
+
export function chartSessionFlags(times: ArrayLike<number>, chart: SessionContext): SessionFlags {
|
|
84
|
+
const s = chartSessions(chart);
|
|
85
|
+
return sessionFlags(Array.from(times, (t) => t * s.unit), s.bars, s.regular);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Higher-timeframe bars of the chart bars and what `mapToChart` needs to bring values back to the chart. */
|
|
89
|
+
export interface SessionResample {
|
|
90
|
+
/** Bars of `timeframe`, built from the chart bars */
|
|
91
|
+
bars: Bar[];
|
|
92
|
+
/** Period index of each chart bar */
|
|
93
|
+
group: number[];
|
|
94
|
+
/** Latest completed period at each chart bar */
|
|
95
|
+
latest: number[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Same-symbol `request.security` on a timeframe equal to or higher than the chart's, with the session of the context:
|
|
100
|
+
* D / W / M periods follow the regular hours, corrections and holidays; intraday periods the loaded session.
|
|
101
|
+
* Compute on `bars`, then bring each value series back with `mapToChart(values, group, latest, lookahead, gaps)`.
|
|
102
|
+
* @throws Error when `timeframe` is lower than the chart timeframe
|
|
103
|
+
*/
|
|
104
|
+
export function resampleSession(bars: Bar[], timeframe: string, chart: SessionContext): SessionResample {
|
|
105
|
+
const s = chartSessions(chart);
|
|
106
|
+
const chartTf = s.bars.chart;
|
|
107
|
+
const tf = info(timeframe || chartTf.period);
|
|
108
|
+
if (tf.period === chartTf.period) {
|
|
109
|
+
const group = bars.map((_, i) => i);
|
|
110
|
+
return { bars, group, latest: group };
|
|
111
|
+
}
|
|
112
|
+
if (in_seconds(tf.period) < in_seconds(chartTf.period)) {
|
|
113
|
+
throw new Error(`resampleSession: timeframe "${tf.period}" is lower than the chart timeframe "${chartTf.period}".`);
|
|
114
|
+
}
|
|
115
|
+
const times = bars.map((b) => (b.time as number) * s.unit);
|
|
116
|
+
const calendar = tf.isdwm ? s.regular : s.bars;
|
|
117
|
+
const starts = times.map((t) => calendar.timeOf(t, tf.period));
|
|
118
|
+
const dayPeriods = times.map((t, i) => {
|
|
119
|
+
const at = s.bars.calendar.locate(t);
|
|
120
|
+
const periods = at ? calendar.calendar.periods(at.date) : [];
|
|
121
|
+
return periods.length ? calendar.timeOf(periods[0]![0], tf.period) : starts[i]!;
|
|
122
|
+
});
|
|
123
|
+
const reaches = times.map((t) => s.bars.closeOf(t, chartTf.period) >= calendar.periodEnd(t, tf.period));
|
|
124
|
+
const { group, latest } = periodsOf(starts, dayPeriods, reaches);
|
|
125
|
+
// a higher-timeframe bar ends at the bar that completes it (no value from after the completion)
|
|
126
|
+
const after = afterCompletion(group, latest);
|
|
127
|
+
const kept = (_: unknown, i: number): boolean => !after[i];
|
|
128
|
+
return { bars: resample(bars.filter(kept), starts.map((x) => x / s.unit).filter(kept)).bars, group, latest };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Open time of the `count`-th bar after the bar that opens at `time`, on the trading calendar of the context: the
|
|
133
|
+
* closed hours, weekends, holidays and days off are skipped. `count` 0 is the bar itself. For a drawing that ends
|
|
134
|
+
* `count` bars after the last bar (PineScript `bar_index + count`).
|
|
135
|
+
*/
|
|
136
|
+
export function barTimeAfter(time: number, count: number, chart: SessionContext): number {
|
|
137
|
+
const s = chartSessions(chart);
|
|
138
|
+
let t = time * s.unit;
|
|
139
|
+
for (let k = 0; k < count && !Number.isNaN(t); k++) t = s.bars.nextBarTime(t);
|
|
140
|
+
return t / s.unit;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Number of bars from the bar that opens at `from` to the bar that contains `to`, on the trading calendar of the
|
|
145
|
+
* context (0 when `to` is in the bar of `from` or before it). For a drawing placed at a time after the last bar
|
|
146
|
+
* (PineScript `time + n days` with `xloc.bar_time`): the chart shows it on the bar of that time, weekends and
|
|
147
|
+
* holidays skipped. NaN when no bar is found within 100,000 bars.
|
|
148
|
+
*/
|
|
149
|
+
export function barsUntil(from: number, to: number, chart: SessionContext): number {
|
|
150
|
+
const s = chartSessions(chart);
|
|
151
|
+
const target = to * s.unit;
|
|
152
|
+
let t = from * s.unit;
|
|
153
|
+
for (let k = 0; k < 100_000; k++) {
|
|
154
|
+
const next = s.bars.nextBarTime(t);
|
|
155
|
+
if (Number.isNaN(next)) return NaN;
|
|
156
|
+
if (next > target) return k;
|
|
157
|
+
t = next;
|
|
158
|
+
}
|
|
159
|
+
return NaN;
|
|
160
|
+
}
|
package/src/str/index.ts
CHANGED
|
@@ -45,7 +45,9 @@ export function length(str: simple_string): int {
|
|
|
45
45
|
*
|
|
46
46
|
* @remarks
|
|
47
47
|
* As in PineScript:
|
|
48
|
-
* - without format, numbers show up to 10 decimals ("0.3333333333")
|
|
48
|
+
* - without format, numbers show up to 10 decimals ("0.3333333333"), rounded on the exact value of the double
|
|
49
|
+
* (82325.10000000005 gives "82325.1"; with the format "#.##########" it gives "82325.1000000001"); 1e21 and more
|
|
50
|
+
* use "1E21"
|
|
49
51
|
* - patterns round half away from zero on the shortest decimal form: 1.005 with "#.##" gives "1.01";
|
|
50
52
|
* at most 16 decimals from 1e-3 up (0.0012345678901234567 with 20 '#' decimals gives "0.0012345678901235")
|
|
51
53
|
* - "pos;neg" patterns: a negative number takes the text of the negative subpattern ("#.##;(#.##)": "(3.5)")
|
|
@@ -72,6 +74,8 @@ export function length(str: simple_string): int {
|
|
|
72
74
|
* ```
|
|
73
75
|
*/
|
|
74
76
|
export function tostring(value: any, format?: simple_string, mintick?: float, pricescale?: float): string {
|
|
77
|
+
// an array: "[a, b]", each element as a single value
|
|
78
|
+
if (Array.isArray(value)) return '[' + value.map((v) => tostring(v, format, mintick, pricescale)).join(', ') + ']';
|
|
75
79
|
if (typeof value !== 'number') return String(value);
|
|
76
80
|
if (Number.isNaN(value)) return format === 'percent' ? 'NaN%' : 'NaN';
|
|
77
81
|
if (!Number.isFinite(value)) {
|
|
@@ -83,7 +87,8 @@ export function tostring(value: any, format?: simple_string, mintick?: float, pr
|
|
|
83
87
|
if (Math.abs(value) >= 1e21 && Number.isFinite(value)) {
|
|
84
88
|
return String(value).replace('e+', 'E').replace(/\.0+E/, 'E');
|
|
85
89
|
}
|
|
86
|
-
|
|
90
|
+
// without a format the exact value is rounded (82325.10000000005 is 82325.10000000004947...: "82325.1")
|
|
91
|
+
return formatNumber(value, '#.##########', 'halfUpExact');
|
|
87
92
|
}
|
|
88
93
|
switch (format) {
|
|
89
94
|
case 'percent':
|
package/src/str/numberformat.ts
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* The pattern syntax is Java's DecimalFormat: `0` required digit, `#` optional digit, `,` grouping,
|
|
5
5
|
* `.` decimal point, `E0` exponent, `%` multiplies by 100, `'...'` quoted literal text.
|
|
6
6
|
* PineScript rules:
|
|
7
|
-
* - `str.tostring` rounds half away from zero on the shortest decimal form of the number
|
|
7
|
+
* - `str.tostring` with a format rounds half away from zero on the shortest decimal form of the number
|
|
8
8
|
* (`1.005` with "#.##" gives "1.01", `2.5` with "#" gives "3")
|
|
9
|
+
* - `str.tostring` without a format rounds the exact binary value to 10 decimals (12.34500000005 is
|
|
10
|
+
* 12.34500000004999975...: "12.345", and "12.3450000001" with the format "#.##########")
|
|
9
11
|
* - `str.format` rounds half to even on the exact binary value (`0.25` with "#.#" gives "0.2")
|
|
10
12
|
* - a negative number that rounds to zero loses its sign; "#.00" writes `.50` (no leading zero),
|
|
11
13
|
* but "#.##" writes `0.5` (a pattern without any '0' shows one integer digit)
|
|
@@ -18,7 +20,7 @@
|
|
|
18
20
|
* 0.1 with 20 '#' decimals gives "0.1" in both rounding modes
|
|
19
21
|
*/
|
|
20
22
|
|
|
21
|
-
export type Rounding = 'halfUpShortest' | 'halfEvenExact';
|
|
23
|
+
export type Rounding = 'halfUpShortest' | 'halfUpExact' | 'halfEvenExact';
|
|
22
24
|
|
|
23
25
|
interface Pattern {
|
|
24
26
|
prefix: string;
|
|
@@ -161,7 +163,7 @@ function roundDigits(d: Digits, frac: number, mode: Rounding): Digits {
|
|
|
161
163
|
if (keep < 0) return { digits: '', intLen: 0 };
|
|
162
164
|
const first = d.digits.charCodeAt(keep) - 48;
|
|
163
165
|
let up: boolean;
|
|
164
|
-
if (mode
|
|
166
|
+
if (mode !== 'halfEvenExact') up = first >= 5;
|
|
165
167
|
else if (first !== 5) up = first > 5;
|
|
166
168
|
else if (d.digits.length > keep + 1) up = true;
|
|
167
169
|
else up = keep > 0 && (d.digits.charCodeAt(keep - 1) - 48) % 2 === 1;
|