oakscriptjs 0.10.1 → 0.11.1
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/array/index.d.ts +8 -2
- package/dist/cjs/array/index.d.ts.map +1 -1
- package/dist/cjs/array/index.js +2 -1
- package/dist/cjs/array/index.js.map +2 -2
- 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 +84 -0
- package/dist/cjs/drawing/registry.d.ts.map +1 -1
- package/dist/cjs/drawing/registry.js +153 -6
- package/dist/cjs/drawing/registry.js.map +2 -2
- package/dist/cjs/index.d.ts +8 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +33 -1
- package/dist/cjs/index.js.map +2 -2
- package/dist/cjs/input.d.ts +9 -1
- package/dist/cjs/input.d.ts.map +1 -1
- package/dist/cjs/input.js +16 -0
- package/dist/cjs/input.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 +10 -27
- package/dist/cjs/line/index.d.ts.map +1 -1
- package/dist/cjs/line/index.js +11 -22
- package/dist/cjs/line/index.js.map +2 -2
- package/dist/cjs/linefill/index.d.ts +3 -0
- package/dist/cjs/linefill/index.d.ts.map +1 -1
- package/dist/cjs/linefill/index.js +15 -1
- package/dist/cjs/linefill/index.js.map +3 -3
- 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/runtime/adapters/SimpleInputAdapter.js +1 -1
- package/dist/cjs/runtime/adapters/SimpleInputAdapter.js.map +2 -2
- package/dist/cjs/runtime/index.d.ts +1 -1
- package/dist/cjs/runtime/index.d.ts.map +1 -1
- package/dist/cjs/runtime/index.js +1 -0
- package/dist/cjs/runtime/index.js.map +2 -2
- package/dist/cjs/runtime/inputs.d.ts +9 -0
- package/dist/cjs/runtime/inputs.d.ts.map +1 -1
- package/dist/cjs/runtime/inputs.js +8 -0
- package/dist/cjs/runtime/inputs.js.map +2 -2
- package/dist/cjs/runtime/types.d.ts +1 -1
- package/dist/cjs/runtime/types.d.ts.map +1 -1
- package/dist/cjs/runtime/types.js.map +1 -1
- package/dist/cjs/script/index.d.ts +91 -21
- package/dist/cjs/script/index.d.ts.map +1 -1
- package/dist/cjs/script/index.js +207 -19
- package/dist/cjs/script/index.js.map +3 -3
- package/dist/cjs/security/other.d.ts +92 -0
- package/dist/cjs/security/other.d.ts.map +1 -0
- package/dist/cjs/security/other.js +164 -0
- package/dist/cjs/security/other.js.map +7 -0
- package/dist/cjs/session/bars.d.ts +24 -1
- package/dist/cjs/session/bars.d.ts.map +1 -1
- package/dist/cjs/session/bars.js +56 -7
- package/dist/cjs/session/bars.js.map +3 -3
- package/dist/cjs/session/calendar.d.ts +13 -2
- package/dist/cjs/session/calendar.d.ts.map +1 -1
- package/dist/cjs/session/calendar.js +39 -9
- package/dist/cjs/session/calendar.js.map +2 -2
- package/dist/cjs/session/context.d.ts +76 -0
- package/dist/cjs/session/context.d.ts.map +1 -0
- package/dist/cjs/session/context.js +114 -0
- package/dist/cjs/session/context.js.map +7 -0
- package/dist/cjs/str/floatingdecimal.d.ts +31 -0
- package/dist/cjs/str/floatingdecimal.d.ts.map +1 -0
- package/dist/cjs/str/floatingdecimal.js +298 -0
- package/dist/cjs/str/floatingdecimal.js.map +7 -0
- package/dist/cjs/str/index.d.ts +4 -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 +7 -3
- package/dist/cjs/str/numberformat.d.ts.map +1 -1
- package/dist/cjs/str/numberformat.js +4 -3
- package/dist/cjs/str/numberformat.js.map +2 -2
- package/dist/cjs/ta/index.d.ts +67 -34
- package/dist/cjs/ta/index.d.ts.map +1 -1
- package/dist/cjs/ta/index.js +202 -145
- package/dist/cjs/ta/index.js.map +2 -2
- package/dist/cjs/ta/limits.d.ts +35 -0
- package/dist/cjs/ta/limits.d.ts.map +1 -0
- package/dist/cjs/ta/limits.js +64 -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 +8 -8
- package/dist/cjs/ta-series.d.ts.map +1 -1
- package/dist/cjs/ta-series.js +10 -8
- package/dist/cjs/ta-series.js.map +2 -2
- package/dist/cjs/table/index.d.ts +3 -1
- package/dist/cjs/table/index.d.ts.map +1 -1
- package/dist/cjs/table/index.js +1 -0
- package/dist/cjs/table/index.js.map +3 -3
- 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 +26 -1
- package/dist/cjs/time/session.d.ts.map +1 -1
- package/dist/cjs/time/session.js +94 -12
- package/dist/cjs/time/session.js.map +3 -3
- package/dist/cjs/time/timezone.d.ts +6 -1
- package/dist/cjs/time/timezone.d.ts.map +1 -1
- package/dist/cjs/time/timezone.js +2 -2
- package/dist/cjs/time/timezone.js.map +2 -2
- package/dist/cjs/types/metadata.d.ts +28 -4
- package/dist/cjs/types/metadata.d.ts.map +1 -1
- package/dist/cjs/types/metadata.js.map +1 -1
- package/dist/esm/array/index.d.ts +8 -2
- package/dist/esm/array/index.d.ts.map +1 -1
- package/dist/esm/array/index.js +12 -3
- package/dist/esm/array/index.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 +84 -0
- package/dist/esm/drawing/registry.d.ts.map +1 -1
- package/dist/esm/drawing/registry.js +260 -6
- package/dist/esm/drawing/registry.js.map +1 -1
- package/dist/esm/index.d.ts +8 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +13 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/input.d.ts +9 -1
- package/dist/esm/input.d.ts.map +1 -1
- package/dist/esm/input.js +16 -0
- package/dist/esm/input.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 +10 -27
- package/dist/esm/line/index.d.ts.map +1 -1
- package/dist/esm/line/index.js +22 -60
- package/dist/esm/line/index.js.map +1 -1
- package/dist/esm/linefill/index.d.ts +3 -0
- package/dist/esm/linefill/index.d.ts.map +1 -1
- package/dist/esm/linefill/index.js +20 -1
- package/dist/esm/linefill/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/runtime/adapters/SimpleInputAdapter.js +1 -1
- package/dist/esm/runtime/adapters/SimpleInputAdapter.js.map +1 -1
- package/dist/esm/runtime/index.d.ts +1 -1
- package/dist/esm/runtime/index.d.ts.map +1 -1
- package/dist/esm/runtime/index.js +1 -1
- package/dist/esm/runtime/index.js.map +1 -1
- package/dist/esm/runtime/inputs.d.ts +9 -0
- package/dist/esm/runtime/inputs.d.ts.map +1 -1
- package/dist/esm/runtime/inputs.js +15 -0
- package/dist/esm/runtime/inputs.js.map +1 -1
- package/dist/esm/runtime/types.d.ts +1 -1
- package/dist/esm/runtime/types.d.ts.map +1 -1
- package/dist/esm/script/index.d.ts +91 -21
- package/dist/esm/script/index.d.ts.map +1 -1
- package/dist/esm/script/index.js +259 -28
- package/dist/esm/script/index.js.map +1 -1
- package/dist/esm/security/other.d.ts +92 -0
- package/dist/esm/security/other.d.ts.map +1 -0
- package/dist/esm/security/other.js +211 -0
- package/dist/esm/security/other.js.map +1 -0
- package/dist/esm/session/bars.d.ts +24 -1
- package/dist/esm/session/bars.d.ts.map +1 -1
- package/dist/esm/session/bars.js +75 -6
- package/dist/esm/session/bars.js.map +1 -1
- package/dist/esm/session/calendar.d.ts +13 -2
- package/dist/esm/session/calendar.d.ts.map +1 -1
- package/dist/esm/session/calendar.js +54 -10
- package/dist/esm/session/calendar.js.map +1 -1
- package/dist/esm/session/context.d.ts +76 -0
- package/dist/esm/session/context.d.ts.map +1 -0
- package/dist/esm/session/context.js +124 -0
- package/dist/esm/session/context.js.map +1 -0
- package/dist/esm/str/floatingdecimal.d.ts +31 -0
- package/dist/esm/str/floatingdecimal.d.ts.map +1 -0
- package/dist/esm/str/floatingdecimal.js +272 -0
- package/dist/esm/str/floatingdecimal.js.map +1 -0
- package/dist/esm/str/index.d.ts +4 -1
- package/dist/esm/str/index.d.ts.map +1 -1
- package/dist/esm/str/index.js +9 -2
- package/dist/esm/str/index.js.map +1 -1
- package/dist/esm/str/numberformat.d.ts +7 -3
- package/dist/esm/str/numberformat.d.ts.map +1 -1
- package/dist/esm/str/numberformat.js +11 -5
- package/dist/esm/str/numberformat.js.map +1 -1
- package/dist/esm/ta/index.d.ts +67 -34
- package/dist/esm/ta/index.d.ts.map +1 -1
- package/dist/esm/ta/index.js +284 -240
- package/dist/esm/ta/index.js.map +1 -1
- package/dist/esm/ta/limits.d.ts +35 -0
- package/dist/esm/ta/limits.d.ts.map +1 -0
- package/dist/esm/ta/limits.js +67 -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 +8 -8
- package/dist/esm/ta-series.d.ts.map +1 -1
- package/dist/esm/ta-series.js +11 -8
- package/dist/esm/ta-series.js.map +1 -1
- package/dist/esm/table/index.d.ts +3 -1
- package/dist/esm/table/index.d.ts.map +1 -1
- package/dist/esm/table/index.js +6 -1
- package/dist/esm/table/index.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 +26 -1
- package/dist/esm/time/session.d.ts.map +1 -1
- package/dist/esm/time/session.js +141 -16
- package/dist/esm/time/session.js.map +1 -1
- package/dist/esm/time/timezone.d.ts +6 -1
- package/dist/esm/time/timezone.d.ts.map +1 -1
- package/dist/esm/time/timezone.js +8 -2
- package/dist/esm/time/timezone.js.map +1 -1
- package/dist/esm/types/metadata.d.ts +28 -4
- package/dist/esm/types/metadata.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/array/index.ts +11 -3
- package/src/box/index.ts +8 -2
- package/src/callsite/index.ts +150 -100
- package/src/drawing/registry.ts +278 -5
- package/src/index.ts +44 -1
- package/src/input.ts +18 -1
- package/src/label/index.ts +4 -1
- package/src/line/index.ts +22 -64
- package/src/linefill/index.ts +20 -1
- package/src/math/index.ts +2 -0
- package/src/polyline/index.ts +3 -1
- package/src/runtime/adapters/SimpleInputAdapter.ts +1 -1
- package/src/runtime/index.ts +1 -0
- package/src/runtime/inputs.ts +16 -0
- package/src/runtime/types.ts +1 -1
- package/src/script/index.ts +355 -34
- package/src/security/other.ts +269 -0
- package/src/session/bars.ts +70 -6
- package/src/session/calendar.ts +60 -10
- package/src/session/context.ts +162 -0
- package/src/str/floatingdecimal.ts +268 -0
- package/src/str/index.ts +8 -2
- package/src/str/numberformat.ts +13 -6
- package/src/ta/index.ts +304 -248
- package/src/ta/limits.ts +74 -0
- package/src/ta/running-sum.ts +2 -0
- package/src/ta-series.ts +19 -16
- package/src/table/index.ts +4 -1
- package/src/time/index.ts +6 -3
- package/src/time/session.ts +137 -15
- package/src/time/timezone.ts +9 -2
- package/src/types/metadata.ts +28 -4
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PineScript `request.security(symbol, timeframe, expression, gaps, lookahead)` of another symbol: which bar of the
|
|
3
|
+
* other symbol each chart bar shows (#179). The bars of the other symbol are given by the caller, already on the
|
|
4
|
+
* requested timeframe; the expression is computed on them and its values are mapped to the chart bars with the index
|
|
5
|
+
* given here. The script API does this with the `bars` accessor of its chart context.
|
|
6
|
+
*
|
|
7
|
+
* PineScript rules (measured on stocks, crypto and an exchange whose trading day starts on the previous UTC day; the
|
|
8
|
+
* chart timeframe, D, W and M on intraday and daily charts; the four gaps / lookahead combinations):
|
|
9
|
+
* - lookahead off: the last bar of the other symbol whose close time is at or before the close time of the chart bar
|
|
10
|
+
* (a daily stock bar closes at the session end, a crypto daily bar at 00:00 UTC: a stock chart shows the crypto
|
|
11
|
+
* bar of the day before, a crypto chart the stock bar of the same day)
|
|
12
|
+
* - lookahead on, intraday chart or intraday request: the last bar of the other symbol that opened at or before the
|
|
13
|
+
* open of the chart bar
|
|
14
|
+
* - lookahead on, D / W / M chart and request: the last bar of the other symbol whose period starts at or before the trading day
|
|
15
|
+
* of the chart bar: the trading day of a D bar, the Monday of a W bar, the first day of the month of an M bar, each
|
|
16
|
+
* in the exchange time zone of its symbol (a week that opens on a Tuesday after a holiday still starts on Monday)
|
|
17
|
+
* - before the first bar of the other symbol: na; on chart bars where the other symbol has no bar (weekends,
|
|
18
|
+
* holidays, closed hours), gaps off repeats the last value
|
|
19
|
+
* - gaps on: a value only on the chart bar where the shown bar changes (and on the first chart bar), na on the others
|
|
20
|
+
* - the last bar while it is open (`lastBarConfirmed` false) with gaps off: the realtime update shows the bar given by
|
|
21
|
+
* lookahead on (the open bar of a higher timeframe), with lookahead off as well
|
|
22
|
+
*
|
|
23
|
+
* Close times, trading days and time zones come from the exchange calendars of the chart and of the other symbol
|
|
24
|
+
* (time zone and session, read by the session functions). Without them:
|
|
25
|
+
* - a symbol that trades every day (bars on Saturdays or Sundays; W bars at Monday 00:00 UTC; M bars on the first day
|
|
26
|
+
* at 00:00 UTC) is on the "24x7" UTC calendar
|
|
27
|
+
* - another symbol whose bars open where the chart calendar starts its periods is on the calendar of the chart
|
|
28
|
+
* - else the UTC rule: an intraday bar closes one bar interval after its open (or at the next bar), a D / W / M bar at
|
|
29
|
+
* the end of the last weekday (UTC) of its day / week / month; the trading day is the UTC date of the bar
|
|
30
|
+
* The fallback is exact between symbols of the same calendar and with 24x7 symbols; between a stock and a 24x7 symbol,
|
|
31
|
+
* or an exchange whose trading day starts on the previous UTC day, it needs the session of the stock.
|
|
32
|
+
*
|
|
33
|
+
* @module security/other
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import * as timeframe from '../timeframe/index.js';
|
|
37
|
+
import { chartSessions, periodClose, periodTime, tradingDayTime, type SessionContext } from '../session/context.js';
|
|
38
|
+
import type { SessionSpec } from '../session/calendar.js';
|
|
39
|
+
|
|
40
|
+
const DAY = 86_400_000;
|
|
41
|
+
|
|
42
|
+
/** Exchange calendar of a symbol: time zone and session (hours, corrections, holidays). */
|
|
43
|
+
export interface SymbolCalendar {
|
|
44
|
+
timezone?: string;
|
|
45
|
+
session?: SessionSpec | string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Options of {@link securityOther}. */
|
|
49
|
+
export interface OtherSymbolOptions {
|
|
50
|
+
/** PineScript `barmerge.lookahead_on`; default false */
|
|
51
|
+
lookahead?: boolean;
|
|
52
|
+
/** PineScript `barmerge.gaps_on`; default false */
|
|
53
|
+
gaps?: boolean;
|
|
54
|
+
/** Calendar of the other symbol; see the module notes for the fallback */
|
|
55
|
+
other?: SymbolCalendar;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The chart: its timeframe, calendar, time unit and whether its last bar is closed. */
|
|
59
|
+
export interface OtherSymbolChart extends SymbolCalendar {
|
|
60
|
+
/** Chart timeframe ("60", "1D"...) */
|
|
61
|
+
timeframe: string;
|
|
62
|
+
/** Unit of the bar times: 's' (default) or 'ms' */
|
|
63
|
+
timeUnit?: 's' | 'ms';
|
|
64
|
+
/** false: the last chart bar is still open (realtime rule) */
|
|
65
|
+
lastBarConfirmed?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface Zone {
|
|
69
|
+
timezone: string;
|
|
70
|
+
session: SessionSpec | string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const calendars = new Map<string, SessionContext | null>();
|
|
74
|
+
|
|
75
|
+
/** The session context of a zone and a timeframe (bar times in ms); null when the session cannot be read. */
|
|
76
|
+
function calendarOf(zone: Zone | null, tf: string): SessionContext | null {
|
|
77
|
+
if (!zone) return null;
|
|
78
|
+
const key = `${zone.timezone}|${typeof zone.session === 'string' ? zone.session : JSON.stringify(zone.session)}|${tf}`;
|
|
79
|
+
let context = calendars.get(key);
|
|
80
|
+
if (context === undefined) {
|
|
81
|
+
context = { timeframe: tf, timezone: zone.timezone, session: zone.session, timeUnit: 'ms' };
|
|
82
|
+
try {
|
|
83
|
+
chartSessions(context);
|
|
84
|
+
} catch {
|
|
85
|
+
context = null;
|
|
86
|
+
}
|
|
87
|
+
if (calendars.size > 64) calendars.clear();
|
|
88
|
+
calendars.set(key, context);
|
|
89
|
+
}
|
|
90
|
+
return context;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const UTC_24X7: Zone = { timezone: 'Etc/UTC', session: '24x7' };
|
|
94
|
+
|
|
95
|
+
/** True when bars of timeframe `tf` opening at `times` (ms) are those of a symbol that trades every day in UTC. */
|
|
96
|
+
function tradesEveryDay(times: number[], tf: string): boolean {
|
|
97
|
+
if (times.length === 0) return false;
|
|
98
|
+
const info = timeframe.info(tf);
|
|
99
|
+
if (info.isweekly) return times.every((t) => t % DAY === 0 && new Date(t).getUTCDay() === 1);
|
|
100
|
+
if (info.ismonthly) return times.every((t) => t % DAY === 0 && new Date(t).getUTCDate() === 1);
|
|
101
|
+
return times.some((t) => {
|
|
102
|
+
const wd = new Date(t).getUTCDay();
|
|
103
|
+
return wd === 0 || wd === 6;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** True when most of the last bars open at the start of a period of `tf` on the calendar `zone`. */
|
|
108
|
+
function onCalendar(times: number[], tf: string, zone: Zone | null): boolean {
|
|
109
|
+
const calendar = calendarOf(zone, tf);
|
|
110
|
+
if (!calendar || times.length === 0) return false;
|
|
111
|
+
const last = times.slice(-50);
|
|
112
|
+
return last.filter((t) => periodTime(t, tf, calendar) === t).length >= 0.9 * last.length;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const utcDay = (t: number): number => Math.floor(t / DAY) * DAY;
|
|
116
|
+
/** Monday 00:00 UTC of the week of the day `day` (00:00 UTC, ms). */
|
|
117
|
+
const weekStart = (day: number): number => day - ((new Date(day).getUTCDay() + 6) % 7) * DAY;
|
|
118
|
+
const monthStart = (day: number): number => {
|
|
119
|
+
const d = new Date(day);
|
|
120
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** 00:00 UTC after the last weekday before `end` (00:00 UTC, ms): the end of a period of a Monday-to-Friday symbol. */
|
|
124
|
+
function afterLastWeekday(end: number): number {
|
|
125
|
+
let day = end - DAY;
|
|
126
|
+
while (new Date(day).getUTCDay() % 6 === 0) day -= DAY;
|
|
127
|
+
return day + DAY;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Close time (ms) of each bar of timeframe `tf` opening at `times` (ms). */
|
|
131
|
+
function closeTimes(times: number[], tf: string, zone: Zone | null): number[] {
|
|
132
|
+
const calendar = calendarOf(zone, tf);
|
|
133
|
+
const info = timeframe.info(tf);
|
|
134
|
+
const len = timeframe.in_seconds(tf) * 1000;
|
|
135
|
+
return times.map((t, i) => {
|
|
136
|
+
if (calendar) {
|
|
137
|
+
const c = periodClose(t, tf, calendar);
|
|
138
|
+
if (!Number.isNaN(c)) return c;
|
|
139
|
+
}
|
|
140
|
+
if (info.isintraday) return i + 1 < times.length ? Math.min(t + len, times[i + 1]!) : t + len;
|
|
141
|
+
const day = utcDay(t);
|
|
142
|
+
if (info.isweekly) return afterLastWeekday(weekStart(day) + 7 * DAY * info.multiplier);
|
|
143
|
+
if (info.ismonthly) {
|
|
144
|
+
const d = new Date(day);
|
|
145
|
+
return afterLastWeekday(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + info.multiplier, 1));
|
|
146
|
+
}
|
|
147
|
+
return day + DAY * info.multiplier;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Trading day (00:00 UTC, ms) of each bar opening at `times` (ms): its date in the exchange time zone. */
|
|
152
|
+
function tradingDays(times: number[], zone: Zone | null): number[] {
|
|
153
|
+
const calendar = calendarOf(zone, '1D');
|
|
154
|
+
return times.map((t) => {
|
|
155
|
+
const d = calendar ? tradingDayTime(t, calendar) : NaN;
|
|
156
|
+
return Number.isNaN(d) ? utcDay(t) : d;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** For each element of `at` (non-decreasing), the index of the last element of `keys` (non-decreasing) <= it; -1: none. */
|
|
161
|
+
function latestAtOrBefore(keys: number[], at: number[]): number[] {
|
|
162
|
+
let j = -1;
|
|
163
|
+
return at.map((x) => {
|
|
164
|
+
while (j + 1 < keys.length && keys[j + 1]! <= x) j++;
|
|
165
|
+
return j;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const zoneOf = (c: SymbolCalendar | undefined): Zone | null =>
|
|
170
|
+
c?.timezone && c.session ? { timezone: c.timezone, session: c.session } : null;
|
|
171
|
+
|
|
172
|
+
/** Timeframes and exchange calendars of a merge: given, 24x7, the chart's, else the UTC rule. */
|
|
173
|
+
function setup(
|
|
174
|
+
ct: number[],
|
|
175
|
+
ot: number[],
|
|
176
|
+
tf: string,
|
|
177
|
+
chart: OtherSymbolChart,
|
|
178
|
+
options: OtherSymbolOptions
|
|
179
|
+
): { chartTf: string; otherTf: string; chartZone: Zone | null; otherZone: Zone | null } {
|
|
180
|
+
const chartTf = timeframe.info(chart.timeframe).period;
|
|
181
|
+
const otherTf = tf.trim() === '' ? chartTf : timeframe.info(tf).period;
|
|
182
|
+
if (timeframe.info(otherTf).isticks) throw new RangeError(`request.security: tick timeframe "${tf}" is not supported.`);
|
|
183
|
+
const chartZone = zoneOf(chart) ?? (tradesEveryDay(ct, chartTf) ? UTC_24X7 : null);
|
|
184
|
+
const otherZone =
|
|
185
|
+
zoneOf(options.other) ??
|
|
186
|
+
(tradesEveryDay(ot, otherTf) ? UTC_24X7 : chartZone && onCalendar(ot, otherTf, chartZone) ? chartZone : null);
|
|
187
|
+
return { chartTf, otherTf, chartZone, otherZone };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* PineScript `request.security` of another symbol: the index in `otherBars` of the bar shown on each chart bar, -1
|
|
192
|
+
* for na (#179). `otherBars` are the bars of the other symbol on `tf` ("" for the chart timeframe, or a higher
|
|
193
|
+
* or a lower one), with times in the unit of the chart bars. Rules in the module notes; the same rules hold for a
|
|
194
|
+
* lower timeframe (#181): lookahead off shows the last lower bar that closes by the close of the chart bar, lookahead
|
|
195
|
+
* on the last one that opened by its open. It also serves the chart symbol on a lower timeframe.
|
|
196
|
+
* @throws RangeError for a tick timeframe
|
|
197
|
+
*/
|
|
198
|
+
export function securityOther(
|
|
199
|
+
chartBars: ReadonlyArray<{ time: number }>,
|
|
200
|
+
otherBars: ReadonlyArray<{ time: number }>,
|
|
201
|
+
tf: string,
|
|
202
|
+
chart: OtherSymbolChart,
|
|
203
|
+
options: OtherSymbolOptions = {}
|
|
204
|
+
): number[] {
|
|
205
|
+
const n = chartBars.length;
|
|
206
|
+
if (n === 0) return [];
|
|
207
|
+
const unit = chart.timeUnit === 'ms' ? 1 : 1000;
|
|
208
|
+
const ct = chartBars.map((b) => b.time * unit);
|
|
209
|
+
const ot = otherBars.map((b) => b.time * unit);
|
|
210
|
+
const { chartTf, otherTf, chartZone, otherZone } = setup(ct, ot, tf, chart, options);
|
|
211
|
+
if (ot.length === 0) return new Array<number>(n).fill(-1);
|
|
212
|
+
|
|
213
|
+
// by trading day when both timeframes are D / W / M; by open time with an intraday side (#181)
|
|
214
|
+
const daily = timeframe.info(chartTf).isdwm && timeframe.info(otherTf).isdwm;
|
|
215
|
+
const realtime = chart.lastBarConfirmed === false && !options.gaps;
|
|
216
|
+
let on: number[] = [];
|
|
217
|
+
if (options.lookahead || realtime) {
|
|
218
|
+
if (daily) {
|
|
219
|
+
const info = timeframe.info(otherTf);
|
|
220
|
+
const starts = tradingDays(ot, otherZone).map((d) => (info.isweekly ? weekStart(d) : info.ismonthly ? monthStart(d) : d));
|
|
221
|
+
on = latestAtOrBefore(starts, tradingDays(ct, chartZone));
|
|
222
|
+
} else {
|
|
223
|
+
on = latestAtOrBefore(ot, ct);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const index = options.lookahead ? on : latestAtOrBefore(closeTimes(ot, otherTf, otherZone), closeTimes(ct, chartTf, chartZone));
|
|
227
|
+
// realtime updates of the open last bar: lookahead off shows the bar of lookahead on
|
|
228
|
+
if (realtime) index[n - 1] = on[n - 1]!;
|
|
229
|
+
if (!options.gaps) return index;
|
|
230
|
+
return index.map((j, i) => (i === 0 || j !== index[i - 1] ? j : -1));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* PineScript `request.security_lower_tf` (#181): for each chart bar, the range `[from, to)` of the bars of
|
|
235
|
+
* `otherBars` (a lower timeframe, the chart symbol or another one) that it contains: the bars whose close time is
|
|
236
|
+
* after the open of the chart bar and at or before its close (a 13:00 hourly bar of a 24x7 symbol is in a 13:30 stock
|
|
237
|
+
* day, which closes at 20:00). An empty range for a chart bar without such bars.
|
|
238
|
+
*/
|
|
239
|
+
export function securityLowerTf(
|
|
240
|
+
chartBars: ReadonlyArray<{ time: number }>,
|
|
241
|
+
otherBars: ReadonlyArray<{ time: number }>,
|
|
242
|
+
tf: string,
|
|
243
|
+
chart: OtherSymbolChart,
|
|
244
|
+
options: Pick<OtherSymbolOptions, 'other'> = {}
|
|
245
|
+
): Array<[number, number]> {
|
|
246
|
+
const unit = chart.timeUnit === 'ms' ? 1 : 1000;
|
|
247
|
+
const ct = chartBars.map((b) => b.time * unit);
|
|
248
|
+
const ot = otherBars.map((b) => b.time * unit);
|
|
249
|
+
const { chartTf, otherTf, chartZone, otherZone } = setup(ct, ot, tf, chart, options);
|
|
250
|
+
const oc = closeTimes(ot, otherTf, otherZone);
|
|
251
|
+
const from = latestAtOrBefore(oc, ct);
|
|
252
|
+
const to = latestAtOrBefore(oc, closeTimes(ct, chartTf, chartZone));
|
|
253
|
+
return ct.map((_, i) => [from[i]! + 1, Math.max(from[i]! + 1, to[i]! + 1)]);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* {@link securityOther} with the values of an expression: `values` has one value per bar of the other symbol; the
|
|
258
|
+
* result has one value per chart bar, NaN for na.
|
|
259
|
+
*/
|
|
260
|
+
export function securityOtherValues(
|
|
261
|
+
chartBars: ReadonlyArray<{ time: number }>,
|
|
262
|
+
otherBars: ReadonlyArray<{ time: number }>,
|
|
263
|
+
tf: string,
|
|
264
|
+
values: ArrayLike<number>,
|
|
265
|
+
chart: OtherSymbolChart,
|
|
266
|
+
options: OtherSymbolOptions = {}
|
|
267
|
+
): number[] {
|
|
268
|
+
return securityOther(chartBars, otherBars, tf, chart, options).map((j) => (j < 0 ? NaN : (values[j] ?? NaN)));
|
|
269
|
+
}
|
package/src/session/bars.ts
CHANGED
|
@@ -111,18 +111,25 @@ 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 and no intraday period: `time("D")`, `time("240")` and `time("60")` are
|
|
117
|
+
* NaN on it (measured; `time("W")` keeps its value).
|
|
118
|
+
*/
|
|
115
119
|
timeOf(time: number, timeframe: string): number {
|
|
116
120
|
const tf = info(timeframe);
|
|
117
121
|
const at = this.calendar.locate(time);
|
|
118
122
|
if (!at) return NaN;
|
|
119
123
|
if (tf.isintraday) {
|
|
120
124
|
if (this.chart.isdwm) return time;
|
|
125
|
+
// a bar after an early close has no intraday period either (#173)
|
|
126
|
+
if (this.stray(time)) return NaN;
|
|
121
127
|
const [start] = this.calendar.periods(at.date)[at.index]!;
|
|
122
128
|
const ms = in_seconds(tf.period) * 1000;
|
|
123
129
|
return start + Math.floor((time - start) / ms) * ms;
|
|
124
130
|
}
|
|
125
131
|
const { unit, n } = unitOf(tf);
|
|
132
|
+
if (unit === 'D' && n === 1 && this.stray(time)) return NaN;
|
|
126
133
|
const [from, to] = this.periodRange(at.date, unit, n);
|
|
127
134
|
const first = this.calendar.firstTradingDay(from, to);
|
|
128
135
|
return first === null ? NaN : this.dayStart(first);
|
|
@@ -137,9 +144,11 @@ export class SessionBars {
|
|
|
137
144
|
const [, end] = this.calendar.periods(at.date)[at.index]!;
|
|
138
145
|
// on D / W / M charts: the end of the session period that contains the bar open
|
|
139
146
|
if (this.chart.isdwm) return end;
|
|
147
|
+
if (this.stray(time)) return NaN;
|
|
140
148
|
return Math.min(this.timeOf(time, timeframe) + in_seconds(tf.period) * 1000, end);
|
|
141
149
|
}
|
|
142
150
|
const { unit, n } = unitOf(tf);
|
|
151
|
+
if (unit === 'D' && n === 1 && this.stray(time)) return NaN;
|
|
143
152
|
const [from, to] = this.periodRange(at.date, unit, n);
|
|
144
153
|
// intraday charts: the start of the next period, except "1D" (the session end)
|
|
145
154
|
if (this.chart.isintraday && (unit !== 'D' || n > 1)) {
|
|
@@ -154,7 +163,7 @@ export class SessionBars {
|
|
|
154
163
|
/** Trading day of a bar: on D / W / M charts the last trading day of the chart period. */
|
|
155
164
|
tradingDay(time: number): DateNum | null {
|
|
156
165
|
const at = this.calendar.locate(time);
|
|
157
|
-
if (!at) return null;
|
|
166
|
+
if (!at || this.stray(time)) return null;
|
|
158
167
|
if (this.chart.isdwm) {
|
|
159
168
|
const { unit, n } = unitOf(this.chart);
|
|
160
169
|
const [from, to] = this.periodRange(at.date, unit, n);
|
|
@@ -188,6 +197,35 @@ export class SessionBars {
|
|
|
188
197
|
return day === null ? NaN : dateUtcMs(day);
|
|
189
198
|
}
|
|
190
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Open time of the bar after the bar that opens at `time`, on the trading calendar: the next bar of the session
|
|
202
|
+
* period, else the start of the next period or trading day; on D / W / M charts the session start of the first
|
|
203
|
+
* trading day of the next chart period. NaN when none is found within 400 days.
|
|
204
|
+
*/
|
|
205
|
+
nextBarTime(time: number): number {
|
|
206
|
+
const at = this.calendar.locate(time);
|
|
207
|
+
if (!at) return NaN;
|
|
208
|
+
if (!this.chart.isdwm) {
|
|
209
|
+
const periods = this.calendar.periods(at.date);
|
|
210
|
+
const [start, end] = periods[at.index]!;
|
|
211
|
+
const ms = in_seconds(this.chart.period) * 1000;
|
|
212
|
+
const next = start + (Math.floor((time - start) / ms) + 1) * ms;
|
|
213
|
+
if (at.inside && next < end) return next;
|
|
214
|
+
if (at.index + 1 < periods.length) return periods[at.index + 1]![0];
|
|
215
|
+
const day = this.calendar.firstTradingDay(addDays(at.date, 1), addDays(at.date, 400));
|
|
216
|
+
return day === null ? NaN : this.dayStart(day);
|
|
217
|
+
}
|
|
218
|
+
const { unit, n } = unitOf(this.chart);
|
|
219
|
+
const [, to] = this.periodRange(at.date, unit, n);
|
|
220
|
+
const day = this.calendar.firstTradingDay(addDays(to, 1), addDays(to, 400));
|
|
221
|
+
return day === null ? NaN : this.dayStart(day);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** An intraday bar that opens after an early close of its day (see TradingCalendar.afterEarlyClose). */
|
|
225
|
+
stray(time: number): boolean {
|
|
226
|
+
return !this.chart.isdwm && this.calendar.afterEarlyClose(time);
|
|
227
|
+
}
|
|
228
|
+
|
|
191
229
|
/** True when the bar at `time` is inside the session. */
|
|
192
230
|
inside(time: number): boolean {
|
|
193
231
|
return this.calendar.locate(time)?.inside ?? false;
|
|
@@ -200,6 +238,25 @@ export class SessionBars {
|
|
|
200
238
|
}
|
|
201
239
|
}
|
|
202
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Periods of the chart bars that open after an early close, for `request.security` (#173): such a bar has no period
|
|
243
|
+
* (`time("D")` and `time("240")` are na on it), but PineScript resamples it into the D / W / M bar of the bar before
|
|
244
|
+
* it, and into an intraday bar of its own (opening at the bar's time). The bar before it still completes its period
|
|
245
|
+
* (its day period is cut there). `intradayMs` is the duration of an intraday timeframe, null for D / W / M.
|
|
246
|
+
* Changes `starts` and `dayPeriods` in place.
|
|
247
|
+
*/
|
|
248
|
+
export function strayPeriods(starts: number[], dayPeriods: number[], times: number[], bars: SessionBars, intradayMs: number | null): void {
|
|
249
|
+
for (let i = 1; i < starts.length; i++) {
|
|
250
|
+
if (!bars.stray(times[i]!)) continue;
|
|
251
|
+
if (Number.isNaN(starts[i]!)) {
|
|
252
|
+
// intraday: periods of `intradayMs` from the first such bar ("240": 18:30 and 20:30 are one bar)
|
|
253
|
+
const run = Number.isNaN(dayPeriods[i - 1]!) && times[i]! < starts[i - 1]! + (intradayMs ?? 0);
|
|
254
|
+
starts[i] = intradayMs === null || run ? starts[i - 1]! : times[i]!;
|
|
255
|
+
}
|
|
256
|
+
dayPeriods[i] = NaN;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
203
260
|
/** PineScript `session.*` flags of each bar. */
|
|
204
261
|
export interface SessionFlags {
|
|
205
262
|
isfirstbar: boolean[];
|
|
@@ -217,6 +274,9 @@ export interface SessionFlags {
|
|
|
217
274
|
* time; first and last bars are per session period (a trading day can have two periods, e.g. after a
|
|
218
275
|
* futures holiday); pre / post market compare with the regular hours of the same trading day; on
|
|
219
276
|
* D / W / M charts every bar is its own session (first, last, market, first and last regular).
|
|
277
|
+
* The first loaded bar is a first bar only when no earlier bar of its period can exist (it opens less than one chart
|
|
278
|
+
* period after the period start). A bar that opens after an early close is in the market hours (the usual hours of
|
|
279
|
+
* its day) but is no last bar.
|
|
220
280
|
*/
|
|
221
281
|
export function sessionFlags(times: number[], bars: SessionBars, regular: SessionBars): SessionFlags {
|
|
222
282
|
const n = times.length;
|
|
@@ -242,18 +302,22 @@ export function sessionFlags(times: number[], bars: SessionBars, regular: Sessio
|
|
|
242
302
|
const close = bars.closeOf(t, chart.period);
|
|
243
303
|
const [, end] = bars.calendar.periods(at.date)[at.index]!;
|
|
244
304
|
const period = `${at.date}/${at.index}`;
|
|
245
|
-
|
|
246
|
-
|
|
305
|
+
const [start] = bars.calendar.periods(at.date)[at.index]!;
|
|
306
|
+
const barMs = in_seconds(chart.period) * 1000;
|
|
307
|
+
// the first loaded bar: a first bar only when no earlier bar of the period can exist
|
|
308
|
+
out.isfirstbar[i] = i === 0 ? t - start < barMs : period !== previousPeriod;
|
|
309
|
+
const stray = bars.stray(t);
|
|
310
|
+
out.islastbar[i] = !stray && close >= end;
|
|
247
311
|
const regularPeriods = regular.calendar.periods(at.date);
|
|
248
312
|
const r = regularPeriods.findIndex(([s, e]) => s <= t && t < e);
|
|
249
313
|
const market = r !== -1;
|
|
250
|
-
out.ismarket[i] = market;
|
|
314
|
+
out.ismarket[i] = market || regular.stray(t);
|
|
251
315
|
if (!market && regularPeriods.length) {
|
|
252
316
|
out.ispremarket[i] = t < regularPeriods[0]![0];
|
|
253
317
|
out.ispostmarket[i] = t >= regularPeriods[regularPeriods.length - 1]![1];
|
|
254
318
|
}
|
|
255
319
|
const regularPeriod = market ? `${at.date}/${r}` : '';
|
|
256
|
-
out.isfirstbar_regular[i] = market && regularPeriod !== previousRegular;
|
|
320
|
+
out.isfirstbar_regular[i] = market && (i === 0 ? t - regularPeriods[r]![0] < barMs : regularPeriod !== previousRegular);
|
|
257
321
|
out.islastbar_regular[i] = market && close >= regularPeriods[r]![1];
|
|
258
322
|
previousPeriod = period;
|
|
259
323
|
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[] {
|
|
@@ -90,11 +94,16 @@ function pineVersion(text: string, from: DateNum): Version {
|
|
|
90
94
|
const ranges = parsePineSession(text);
|
|
91
95
|
if (ranges === null) throw new SyntaxError(`Session "${text}" is the symbol session`);
|
|
92
96
|
const byDay: Period[][] = Array.from({ length: 8 }, () => []);
|
|
93
|
-
for (const { start, end, days } of ranges) {
|
|
97
|
+
for (const { start, end, days, startDays } of ranges) {
|
|
98
|
+
// a run that ends at 0000 ends at 24:00 of the day it starts and belongs to that day (#173)
|
|
94
99
|
const period: Period =
|
|
95
100
|
start === 0 && end === 0
|
|
96
101
|
? { start: 0, end: 1440, startOffset: 0, endOffset: 0 }
|
|
97
|
-
:
|
|
102
|
+
: startDays !== undefined
|
|
103
|
+
? { start, end, startOffset: startDays, endOffset: 0 }
|
|
104
|
+
: end === 0
|
|
105
|
+
? { start, end: 1440, startOffset: 0, endOffset: 0 }
|
|
106
|
+
: { start, end, startOffset: end <= start ? 1 : 0, endOffset: 0 };
|
|
98
107
|
for (const d of days) byDay[d]!.push(period);
|
|
99
108
|
}
|
|
100
109
|
return { from, periods: [], days: byDay.map((p) => p.length > 0), byDay };
|
|
@@ -102,6 +111,17 @@ function pineVersion(text: string, from: DateNum): Version {
|
|
|
102
111
|
|
|
103
112
|
function parseVersion(text: string, from: DateNum, spec: string, rule: 'symbol' | 'pine'): Version {
|
|
104
113
|
if (rule === 'pine') return pineVersion(text, from);
|
|
114
|
+
if (text.includes('|')) {
|
|
115
|
+
// sections with their own days
|
|
116
|
+
const byDay: Period[][] = Array.from({ length: 8 }, () => []);
|
|
117
|
+
for (const section of text.split('|').filter((part) => part.trim())) {
|
|
118
|
+
const one = parseVersion(section, from, spec, rule);
|
|
119
|
+
one.days.forEach((on, d) => {
|
|
120
|
+
if (on) byDay[d] = one.periods;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return { from, periods: [], days: byDay.map((p) => p.length > 0), byDay };
|
|
124
|
+
}
|
|
105
125
|
const [periodText = '', dayText] = text.trim().split(':');
|
|
106
126
|
const periods = parsePeriods(periodText, spec);
|
|
107
127
|
const allDays = periodText.trim() === '24x7';
|
|
@@ -118,17 +138,22 @@ export class TradingCalendar {
|
|
|
118
138
|
private readonly corrections = new Map<DateNum, Period[] | null>();
|
|
119
139
|
private readonly holidays: Set<DateNum>;
|
|
120
140
|
private readonly cache = new Map<DateNum, Array<[number, number]>>();
|
|
141
|
+
private readonly pine: boolean;
|
|
121
142
|
|
|
122
143
|
/**
|
|
123
144
|
* @param timezone - Time zone of the session hours (the exchange time zone)
|
|
124
145
|
* @param spec - Session information (hours, corrections, holidays)
|
|
125
146
|
* @param rule - Days without ":days": 'symbol' (Monday to Friday) or 'pine' (PineScript session strings)
|
|
147
|
+
* @param dayPlacement - PineScript session strings read for `time("D", session)`: the wall times of a clock-change
|
|
148
|
+
* day are placed as the JVM does (see `periods`); without it, as `time.inSession` places them
|
|
126
149
|
*/
|
|
127
150
|
constructor(
|
|
128
151
|
readonly timezone: string,
|
|
129
152
|
spec: SessionSpec,
|
|
130
|
-
rule: 'symbol' | 'pine' = 'symbol'
|
|
153
|
+
rule: 'symbol' | 'pine' = 'symbol',
|
|
154
|
+
dayPlacement = false
|
|
131
155
|
) {
|
|
156
|
+
this.pine = rule === 'pine' && dayPlacement;
|
|
132
157
|
const [first = '', ...changes] = spec.session.split('#');
|
|
133
158
|
this.versions = [parseVersion(first, 0, spec.session, rule)];
|
|
134
159
|
for (const change of changes) {
|
|
@@ -162,10 +187,16 @@ export class TradingCalendar {
|
|
|
162
187
|
const y = Math.floor(date / 10000);
|
|
163
188
|
const m = Math.floor(date / 100) % 100;
|
|
164
189
|
const d = date % 100;
|
|
165
|
-
|
|
166
|
-
zonedToUnix(this.timezone, y, m,
|
|
167
|
-
|
|
168
|
-
|
|
190
|
+
const at = (dd: number, minutes: number, earlier = false): number =>
|
|
191
|
+
zonedToUnix(this.timezone, y, m, dd, Math.floor(minutes / 60), minutes % 60, 0, earlier);
|
|
192
|
+
// for "D", PineScript session strings place the wall times of a clock-change day as the JVM does: a skipped time moves
|
|
193
|
+
// past the gap, a repeated time is its first pass, and the end is the minute before it plus one minute ("0000-0300"
|
|
194
|
+
// on a 02:00 -> 03:00 night ends at 03:00 of the old offset, "0000-0400" at 04:00 of the new one) (#173)
|
|
195
|
+
out = periods.map((p) =>
|
|
196
|
+
this.pine
|
|
197
|
+
? [at(d - p.startOffset, p.start, true), at(d - p.endOffset, p.end - 1, true) + 60_000]
|
|
198
|
+
: [at(d - p.startOffset, p.start), at(d - p.endOffset, p.end)]
|
|
199
|
+
);
|
|
169
200
|
this.cache.set(date, out);
|
|
170
201
|
return out;
|
|
171
202
|
}
|
|
@@ -174,6 +205,25 @@ export class TradingCalendar {
|
|
|
174
205
|
return this.periods(date).length > 0;
|
|
175
206
|
}
|
|
176
207
|
|
|
208
|
+
/**
|
|
209
|
+
* True for a bar that opens after the session of its day was cut by a correction (an early close) but inside the
|
|
210
|
+
* usual hours of that day: PineScript has no period for such a bar (`time("D")` is na on it).
|
|
211
|
+
*/
|
|
212
|
+
afterEarlyClose(time: number): boolean {
|
|
213
|
+
const at = this.locate(time);
|
|
214
|
+
if (!at || at.inside || !this.corrections.get(at.date)) return false;
|
|
215
|
+
const version = [...this.versions].reverse().find((v) => v.from <= at.date)!;
|
|
216
|
+
const usual = version.byDay ? version.byDay[weekdayOf(at.date)]! : version.days[weekdayOf(at.date)] ? version.periods : [];
|
|
217
|
+
const y = Math.floor(at.date / 10000);
|
|
218
|
+
const m = Math.floor(at.date / 100) % 100;
|
|
219
|
+
const d = at.date % 100;
|
|
220
|
+
return usual.some(
|
|
221
|
+
(p) =>
|
|
222
|
+
zonedToUnix(this.timezone, y, m, d - p.startOffset, Math.floor(p.start / 60), p.start % 60) <= time &&
|
|
223
|
+
time < zonedToUnix(this.timezone, y, m, d - p.endOffset, Math.floor(p.end / 60), p.end % 60)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
177
227
|
/**
|
|
178
228
|
* Trading day and period of a time: the period that contains it, else the last period that started
|
|
179
229
|
* before it. Null when no trading day is found near the time.
|