@hoox-sh/pynets 0.1.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/CHANGELOG.md +30 -0
- package/LICENSE +661 -0
- package/NOTICE +46 -0
- package/README.md +327 -0
- package/dist/browser.js +44 -0
- package/dist/index.d.ts +348 -0
- package/dist/index.js +41741 -0
- package/package.json +77 -0
- package/src/ast/builder.ts +1002 -0
- package/src/ast/dump.ts +43 -0
- package/src/ast/helper.ts +77 -0
- package/src/ast/nodes.ts +570 -0
- package/src/ast/unparse.ts +360 -0
- package/src/cli/rich.ts +378 -0
- package/src/cli.ts +627 -0
- package/src/generated/PinescriptLexer.interp +267 -0
- package/src/generated/PinescriptLexer.tokens +131 -0
- package/src/generated/PinescriptLexer.ts +673 -0
- package/src/generated/PinescriptLexerBase.ts +395 -0
- package/src/generated/PinescriptParser.interp +267 -0
- package/src/generated/PinescriptParser.tokens +131 -0
- package/src/generated/PinescriptParser.ts +8863 -0
- package/src/generated/PinescriptParserBase.ts +21 -0
- package/src/generated/PinescriptParserVisitor.ts +840 -0
- package/src/index.ts +55 -0
- package/src/parser/LexerBase.ts +395 -0
- package/src/parser/ParserBase.ts +21 -0
- package/src/parser/error.ts +33 -0
- package/src/runtime/array.ts +486 -0
- package/src/runtime/color.ts +164 -0
- package/src/runtime/drawings.ts +510 -0
- package/src/runtime/input.ts +102 -0
- package/src/runtime/interpret.ts +3897 -0
- package/src/runtime/library.ts +282 -0
- package/src/runtime/log.ts +69 -0
- package/src/runtime/map.ts +85 -0
- package/src/runtime/math.ts +231 -0
- package/src/runtime/matrix.ts +1154 -0
- package/src/runtime/provider.ts +232 -0
- package/src/runtime/request.ts +372 -0
- package/src/runtime/series.ts +44 -0
- package/src/runtime/str.ts +433 -0
- package/src/runtime/strategy.ts +1105 -0
- package/src/runtime/ta.ts +2093 -0
- package/src/runtime/ticker.ts +81 -0
- package/src/runtime/time.ts +439 -0
- package/src/runtime/timeframe.ts +85 -0
- package/src/runtime/udt.ts +119 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2024-2026 jango_blockchained
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*
|
|
5
|
+
* Pine `ticker.*` symbol ids. Stringify to the symbol (Python `TickerInfo`).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class TickerId {
|
|
9
|
+
heikinashi = false;
|
|
10
|
+
renko = false;
|
|
11
|
+
kagi = false;
|
|
12
|
+
linebreak = false;
|
|
13
|
+
pointfigure = false;
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
public symbol: string,
|
|
17
|
+
public session?: string,
|
|
18
|
+
public adjust?: string,
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
toString(): string {
|
|
22
|
+
return this.symbol;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function tickerNew(symbol: string, session?: string, adjust?: string): TickerId {
|
|
27
|
+
return new TickerId(symbol, session, adjust);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function tickerHeikinashi(symbol: string): TickerId {
|
|
31
|
+
const t = new TickerId(typeof symbol === "string" ? symbol : String(symbol));
|
|
32
|
+
t.heikinashi = true;
|
|
33
|
+
return t;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function tickerStandard(symbol: string): TickerId {
|
|
37
|
+
return new TickerId(symbol);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function tickerRenko(symbol: string): TickerId {
|
|
41
|
+
const t = new TickerId(symbol);
|
|
42
|
+
t.renko = true;
|
|
43
|
+
return t;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function tickerKagi(symbol: string): TickerId {
|
|
47
|
+
const t = new TickerId(typeof symbol === "string" ? symbol : String(symbol));
|
|
48
|
+
t.kagi = true;
|
|
49
|
+
return t;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function tickerLinebreak(symbol: string): TickerId {
|
|
53
|
+
const t = new TickerId(typeof symbol === "string" ? symbol : String(symbol));
|
|
54
|
+
t.linebreak = true;
|
|
55
|
+
return t;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function tickerPointfigure(symbol: string): TickerId {
|
|
59
|
+
const t = new TickerId(typeof symbol === "string" ? symbol : String(symbol));
|
|
60
|
+
t.pointfigure = true;
|
|
61
|
+
return t;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Optional fields keep the previous value. `adjustment` is a Python alias for `adjust`. */
|
|
65
|
+
export function tickerModify(
|
|
66
|
+
ticker: TickerId | string,
|
|
67
|
+
opts?: { symbol?: string; session?: string; adjust?: string; adjustment?: string },
|
|
68
|
+
): TickerId {
|
|
69
|
+
const src = typeof ticker === "string" ? new TickerId(ticker) : ticker;
|
|
70
|
+
const t = new TickerId(
|
|
71
|
+
opts?.symbol ?? src.symbol,
|
|
72
|
+
opts?.session ?? src.session,
|
|
73
|
+
opts?.adjust ?? opts?.adjustment ?? src.adjust,
|
|
74
|
+
);
|
|
75
|
+
t.heikinashi = src.heikinashi;
|
|
76
|
+
t.renko = src.renko;
|
|
77
|
+
t.kagi = src.kagi;
|
|
78
|
+
t.linebreak = src.linebreak;
|
|
79
|
+
t.pointfigure = src.pointfigure;
|
|
80
|
+
return t;
|
|
81
|
+
}
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2024-2026 jango_blockchained
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*
|
|
5
|
+
* UTC calendar parts and Pine `timestamp` / `weekofyear` / `time_tradingday`.
|
|
6
|
+
* Python `utility.py` / `time_parts.utc_parts_from_ms` SoT. `null` is `na`.
|
|
7
|
+
* Pine `dayofweek`: 1=Sunday … 7=Saturday.
|
|
8
|
+
*/
|
|
9
|
+
export interface UtcParts {
|
|
10
|
+
year: number;
|
|
11
|
+
month: number;
|
|
12
|
+
dayofmonth: number;
|
|
13
|
+
hour: number;
|
|
14
|
+
minute: number;
|
|
15
|
+
second: number;
|
|
16
|
+
dayofweek: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MIN_T = -62_167_219_200; // ~year 0001
|
|
20
|
+
const MAX_T = 253_402_300_799; // ~year 9999
|
|
21
|
+
|
|
22
|
+
/** Unix-ms → UTC parts. Matches Python `utc_parts_from_ms`. */
|
|
23
|
+
export function utcPartsFromMs(ms: number): UtcParts {
|
|
24
|
+
let t = Number.isFinite(ms) ? Math.trunc(ms / 1000) : 0;
|
|
25
|
+
if (t < MIN_T) t = MIN_T;
|
|
26
|
+
else if (t > MAX_T) t = MAX_T;
|
|
27
|
+
|
|
28
|
+
let days = Math.trunc(t / 86_400);
|
|
29
|
+
let rem = t % 86_400;
|
|
30
|
+
if (rem < 0) {
|
|
31
|
+
days -= 1;
|
|
32
|
+
rem += 86_400;
|
|
33
|
+
}
|
|
34
|
+
const hour = Math.trunc(rem / 3600);
|
|
35
|
+
rem %= 3600;
|
|
36
|
+
const minute = Math.trunc(rem / 60);
|
|
37
|
+
const second = rem % 60;
|
|
38
|
+
|
|
39
|
+
// Epoch day 0 (1970-01-01) Thursday. Python weekday Mon=0.
|
|
40
|
+
const weekday = ((days + 3) % 7 + 7) % 7; // 0=Mon … 6=Sun
|
|
41
|
+
const dayofweek = ((weekday + 1) % 7) + 1; // Pine 1=Sun … 7=Sat
|
|
42
|
+
|
|
43
|
+
// Civil date from days since Unix epoch (Howard Hinnant).
|
|
44
|
+
const z = days + 719_468;
|
|
45
|
+
const era = Math.trunc((z >= 0 ? z : z - 146_096) / 146_097);
|
|
46
|
+
const doe = z - era * 146_097;
|
|
47
|
+
const yoe = Math.trunc((doe - Math.trunc(doe / 1460) + Math.trunc(doe / 36_524) - Math.trunc(doe / 146_096)) / 365);
|
|
48
|
+
const y = yoe + era * 400;
|
|
49
|
+
const doy = doe - (365 * yoe + Math.trunc(yoe / 4) - Math.trunc(yoe / 100));
|
|
50
|
+
const mp = Math.trunc((5 * doy + 2) / 153);
|
|
51
|
+
const day = doy - Math.trunc((153 * mp + 2) / 5) + 1;
|
|
52
|
+
const month = mp < 10 ? mp + 3 : mp - 9;
|
|
53
|
+
const year = y + (month <= 2 ? 1 : 0);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
year,
|
|
57
|
+
month,
|
|
58
|
+
dayofmonth: day,
|
|
59
|
+
hour,
|
|
60
|
+
minute,
|
|
61
|
+
second,
|
|
62
|
+
dayofweek,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const MS_PER_DAY = 86_400_000;
|
|
67
|
+
const SEC_PER_DAY = 86_400;
|
|
68
|
+
|
|
69
|
+
const MONTH_INDEX: Record<string, number> = {
|
|
70
|
+
jan: 1,
|
|
71
|
+
january: 1,
|
|
72
|
+
feb: 2,
|
|
73
|
+
february: 2,
|
|
74
|
+
mar: 3,
|
|
75
|
+
march: 3,
|
|
76
|
+
apr: 4,
|
|
77
|
+
april: 4,
|
|
78
|
+
may: 5,
|
|
79
|
+
jun: 6,
|
|
80
|
+
june: 6,
|
|
81
|
+
jul: 7,
|
|
82
|
+
july: 7,
|
|
83
|
+
aug: 8,
|
|
84
|
+
august: 8,
|
|
85
|
+
sep: 9,
|
|
86
|
+
september: 9,
|
|
87
|
+
oct: 10,
|
|
88
|
+
october: 10,
|
|
89
|
+
nov: 11,
|
|
90
|
+
november: 11,
|
|
91
|
+
dec: 12,
|
|
92
|
+
december: 12,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
function floorDiv(a: number, b: number): number {
|
|
96
|
+
return Math.floor(a / b);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function floorMod(a: number, b: number): number {
|
|
100
|
+
return ((a % b) + b) % b;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Days since Unix epoch for civil y-m-d (Howard Hinnant). Valid dates only. */
|
|
104
|
+
function daysFromCivil(y: number, m: number, d: number): number {
|
|
105
|
+
y -= m <= 2 ? 1 : 0;
|
|
106
|
+
const era = floorDiv(y >= 0 ? y : y - 399, 400);
|
|
107
|
+
const yoe = y - era * 400;
|
|
108
|
+
const doy = floorDiv(153 * (m + (m > 2 ? -3 : 9)) + 2, 5) + d - 1;
|
|
109
|
+
const doe = yoe * 365 + floorDiv(yoe, 4) - floorDiv(yoe, 100) + doy;
|
|
110
|
+
return era * 146_097 + doe - 719_468;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isLeapYear(year: number): boolean {
|
|
114
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function daysInMonth(year: number, month: number): number {
|
|
118
|
+
if (month === 2) return isLeapYear(year) ? 29 : 28;
|
|
119
|
+
return month === 4 || month === 6 || month === 9 || month === 11 ? 30 : 31;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function utcMsFromYmdHms(year: number, month: number, day: number, hour: number, minute: number, second: number): number {
|
|
123
|
+
return (daysFromCivil(year, month, day) * SEC_PER_DAY + hour * 3600 + minute * 60 + second) * 1000;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Month 0 → January of the same year. 13+ / negatives roll years.
|
|
128
|
+
* Year clamped to 1..9999. Floats already truncated by the caller.
|
|
129
|
+
*/
|
|
130
|
+
function normalizeYearMonth(year: number, month: number): [number, number] {
|
|
131
|
+
let y = year;
|
|
132
|
+
let m = month;
|
|
133
|
+
if (m === 0) {
|
|
134
|
+
m = 1;
|
|
135
|
+
} else if (m < 1 || m > 12) {
|
|
136
|
+
const m0 = m - 1;
|
|
137
|
+
y += floorDiv(m0, 12);
|
|
138
|
+
m = floorMod(m0, 12) + 1;
|
|
139
|
+
}
|
|
140
|
+
if (y < 1) y = 1;
|
|
141
|
+
else if (y > 9999) y = 9999;
|
|
142
|
+
return [y, m];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function requiredTrunc(value: unknown): number | null {
|
|
146
|
+
if (value == null) return null;
|
|
147
|
+
if (typeof value === "number") return Number.isFinite(value) ? Math.trunc(value) : null;
|
|
148
|
+
if (typeof value === "string") {
|
|
149
|
+
const s = value.trim();
|
|
150
|
+
if (!s) return null;
|
|
151
|
+
const n = Number(s);
|
|
152
|
+
return Number.isFinite(n) ? Math.trunc(n) : null;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function optionalTrunc(value: unknown, fallback = 0): number {
|
|
158
|
+
if (value == null) return fallback;
|
|
159
|
+
if (typeof value === "number") return Number.isFinite(value) ? Math.trunc(value) : fallback;
|
|
160
|
+
if (typeof value === "string") {
|
|
161
|
+
const s = value.trim();
|
|
162
|
+
if (!s) return fallback;
|
|
163
|
+
const n = Number(s);
|
|
164
|
+
return Number.isFinite(n) ? Math.trunc(n) : fallback;
|
|
165
|
+
}
|
|
166
|
+
return fallback;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function offsetFromHm(sign: string, hours: number, mins: number): number {
|
|
170
|
+
const mult = sign === "+" ? 1 : -1;
|
|
171
|
+
return mult * (hours * 60 + mins);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function offsetFromDigits(sign: string, digits: string): number | null {
|
|
175
|
+
if (!digits || digits.length > 4 || !/^\d+$/.test(digits)) return null;
|
|
176
|
+
if (digits.length <= 2) return offsetFromHm(sign, Number(digits), 0);
|
|
177
|
+
const padded = digits.padStart(4, "0");
|
|
178
|
+
return offsetFromHm(sign, Number(padded.slice(0, 2)), Number(padded.slice(2)));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Pine `UTC` / `GMT` / `UTC-5` / `GMT+10` / `UTC+5:30`. Unknown → 0 (UTC). */
|
|
182
|
+
function parseTimezoneOffsetMinutes(tzSpec: string): number {
|
|
183
|
+
const z = tzSpec.trim();
|
|
184
|
+
if (!z || /^(?:syminfo\.timezone|UTC|utc|Etc\/UTC|GMT|gmt)$/.test(z)) return 0;
|
|
185
|
+
const m = /^(?:(?:UTC|GMT)\s*)?([+-])\s*(\d{1,2})(?::(\d{2})|(\d{2}))?$/i.exec(z);
|
|
186
|
+
if (!m) return 0;
|
|
187
|
+
return offsetFromHm(m[1]!, Number(m[2]), Number(m[3] || m[4] || 0));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function normalizeTimestampText(s: string): string {
|
|
191
|
+
let out = s.trim().replace(/\s+/g, " ");
|
|
192
|
+
out = out.replace(/\b0000-/, "0001-");
|
|
193
|
+
out = out.replace(/(\d{4}-\d{2}-\d{2}):(\d{1,2}:\d{2})/, "$1 $2");
|
|
194
|
+
out = out.replace(/(\d)([A-Za-z]{3,})/g, "$1 $2");
|
|
195
|
+
out = out.replace(/\bSept\b/gi, "Sep");
|
|
196
|
+
out = out.replace(/\bJanv\b/gi, "Jan");
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function stripTimezone(s: string): { date: string; offsetMin: number } {
|
|
201
|
+
let offsetMin = 0;
|
|
202
|
+
const lead = /^(?:UTC|GMT)\s+/i.exec(s);
|
|
203
|
+
if (lead) s = s.slice(lead[0].length).trim();
|
|
204
|
+
|
|
205
|
+
let m = /\s*(?:GMT|UTC)\s*([+-])(\d{1,2})(?::?(\d{2}))?\s*$/i.exec(s);
|
|
206
|
+
if (m && m.index !== undefined) {
|
|
207
|
+
offsetMin = offsetFromHm(m[1]!, Number(m[2]), m[3] ? Number(m[3]) : 0);
|
|
208
|
+
return { date: s.slice(0, m.index).trim(), offsetMin };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
m = /Z\s*$/i.exec(s);
|
|
212
|
+
if (m && s.length > 1 && m.index !== undefined) {
|
|
213
|
+
return { date: s.slice(0, m.index).trim(), offsetMin };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
m = /\s+([+-])(\d{1,2}):(\d{2})\s*$/.exec(s);
|
|
217
|
+
if (m && m.index !== undefined) {
|
|
218
|
+
offsetMin = offsetFromHm(m[1]!, Number(m[2]), Number(m[3]));
|
|
219
|
+
return { date: s.slice(0, m.index).trim(), offsetMin };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
m = /\s+([+-])(\d{1,4})\s*$/.exec(s);
|
|
223
|
+
if (m && m.index !== undefined) {
|
|
224
|
+
const off = offsetFromDigits(m[1]!, m[2]!);
|
|
225
|
+
if (off != null) return { date: s.slice(0, m.index).trim(), offsetMin: off };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
m = /(?<=\d)([+-])(\d{2}):(\d{2})\s*$/.exec(s);
|
|
229
|
+
if (m && m.index !== undefined) {
|
|
230
|
+
offsetMin = offsetFromHm(m[1]!, Number(m[2]), Number(m[3]));
|
|
231
|
+
return { date: s.slice(0, m.index).trim(), offsetMin };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
m = /(?<=\d)([+-])(\d{4})\s*$/.exec(s);
|
|
235
|
+
if (m && m.index !== undefined) {
|
|
236
|
+
const off = offsetFromDigits(m[1]!, m[2]!);
|
|
237
|
+
if (off != null) return { date: s.slice(0, m.index).trim(), offsetMin: off };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
m = /(?<=\d)([+-])(\d{2})\s*$/.exec(s);
|
|
241
|
+
if (m && m.index !== undefined) {
|
|
242
|
+
const head = s.slice(0, m.index);
|
|
243
|
+
if (/\d{1,2}:\d{2}$/.test(head) || head.endsWith("T") || /T\d/.test(head)) {
|
|
244
|
+
const off = offsetFromDigits(m[1]!, m[2]!);
|
|
245
|
+
if (off != null) return { date: head.trim(), offsetMin: off };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { date: s.replace(/\s+(?:GMT|UTC)\s*$/i, "").trim(), offsetMin };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function civilMsStrict(
|
|
253
|
+
year: number,
|
|
254
|
+
month: number,
|
|
255
|
+
day: number,
|
|
256
|
+
hour: number,
|
|
257
|
+
minute: number,
|
|
258
|
+
second: number,
|
|
259
|
+
): number | null {
|
|
260
|
+
if (year < 1 || year > 9999) return null;
|
|
261
|
+
if (month < 1 || month > 12) return null;
|
|
262
|
+
if (day < 1 || day > daysInMonth(year, month)) return null;
|
|
263
|
+
if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) return null;
|
|
264
|
+
return utcMsFromYmdHms(year, month, day, hour, minute, second);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function parseNaiveDateTime(s: string): number | null {
|
|
268
|
+
let m = /^([A-Za-z]+)\s+(\d{1,2}),?\s+(\d{4})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/.exec(s);
|
|
269
|
+
if (m) {
|
|
270
|
+
const month = MONTH_INDEX[m[1]!.toLowerCase()];
|
|
271
|
+
if (month == null) return null;
|
|
272
|
+
return civilMsStrict(Number(m[3]), month, Number(m[2]), Number(m[4] ?? 0), Number(m[5] ?? 0), Number(m[6] ?? 0));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
m = /^(\d{1,2})\s+([A-Za-z]+),?\s+(\d{4})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/.exec(s);
|
|
276
|
+
if (m) {
|
|
277
|
+
const month = MONTH_INDEX[m[2]!.toLowerCase()];
|
|
278
|
+
if (month == null) return null;
|
|
279
|
+
return civilMsStrict(Number(m[3]), month, Number(m[1]), Number(m[4] ?? 0), Number(m[5] ?? 0), Number(m[6] ?? 0));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
m = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2}))?)?$/.exec(s);
|
|
283
|
+
if (m) {
|
|
284
|
+
return civilMsStrict(
|
|
285
|
+
Number(m[1]),
|
|
286
|
+
Number(m[2]),
|
|
287
|
+
Number(m[3]),
|
|
288
|
+
Number(m[4] ?? 0),
|
|
289
|
+
Number(m[5] ?? 0),
|
|
290
|
+
Number(m[6] ?? 0),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
m = /^(\d{4})\s+(\d{1,2})\s+(\d{1,2})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/.exec(s);
|
|
295
|
+
if (m) {
|
|
296
|
+
return civilMsStrict(
|
|
297
|
+
Number(m[1]),
|
|
298
|
+
Number(m[2]),
|
|
299
|
+
Number(m[3]),
|
|
300
|
+
Number(m[4] ?? 0),
|
|
301
|
+
Number(m[5] ?? 0),
|
|
302
|
+
Number(m[6] ?? 0),
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function tryTimestampFormats(s: string, offsetMin: number): number | null {
|
|
310
|
+
const local = parseNaiveDateTime(s);
|
|
311
|
+
if (local == null) return null;
|
|
312
|
+
return local - offsetMin * 60_000;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Python `date.toordinal()` (0001-01-01 = 1). */
|
|
316
|
+
function ymdToOrd(year: number, month: number, day: number): number {
|
|
317
|
+
return daysFromCivil(year, month, day) + 719_163;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function isoWeek1Monday(year: number): number {
|
|
321
|
+
const firstday = ymdToOrd(year, 1, 1);
|
|
322
|
+
const firstweekday = floorMod(firstday + 6, 7);
|
|
323
|
+
let week1monday = firstday - firstweekday;
|
|
324
|
+
if (firstweekday > 3) week1monday += 7;
|
|
325
|
+
return week1monday;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function isoWeekOfYear(year: number, month: number, day: number): number {
|
|
329
|
+
const today = ymdToOrd(year, month, day);
|
|
330
|
+
let week1monday = isoWeek1Monday(year);
|
|
331
|
+
let week = floorDiv(today - week1monday, 7);
|
|
332
|
+
if (week < 0) {
|
|
333
|
+
week1monday = isoWeek1Monday(year - 1);
|
|
334
|
+
week = floorDiv(today - week1monday, 7);
|
|
335
|
+
} else if (week >= 52 && today >= isoWeek1Monday(year + 1)) {
|
|
336
|
+
week = 0;
|
|
337
|
+
}
|
|
338
|
+
return week + 1;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Unix ms for calendar components. Month 0 → January; day/hour/min/sec overflow
|
|
343
|
+
* via timedelta on a day-1 midnight anchor. Optional offset: components are local.
|
|
344
|
+
*/
|
|
345
|
+
export function timestampFromComponents(
|
|
346
|
+
year: number | null | undefined,
|
|
347
|
+
month: number | null | undefined,
|
|
348
|
+
day: number | null | undefined,
|
|
349
|
+
hour: number | null | undefined = 0,
|
|
350
|
+
minute: number | null | undefined = 0,
|
|
351
|
+
second: number | null | undefined = 0,
|
|
352
|
+
tzOffsetMinutes?: number | null,
|
|
353
|
+
): number | null {
|
|
354
|
+
const yIn = requiredTrunc(year);
|
|
355
|
+
const mIn = requiredTrunc(month);
|
|
356
|
+
const dIn = requiredTrunc(day);
|
|
357
|
+
if (yIn == null || mIn == null || dIn == null) return null;
|
|
358
|
+
const h = optionalTrunc(hour, 0);
|
|
359
|
+
const mi = optionalTrunc(minute, 0);
|
|
360
|
+
const s = optionalTrunc(second, 0);
|
|
361
|
+
const [y, m] = normalizeYearMonth(yIn, mIn);
|
|
362
|
+
const baseSec = daysFromCivil(y, m, 1) * SEC_PER_DAY;
|
|
363
|
+
let utcSec = baseSec + (dIn - 1) * SEC_PER_DAY + h * 3600 + mi * 60 + s;
|
|
364
|
+
if (tzOffsetMinutes != null && Number.isFinite(tzOffsetMinutes)) {
|
|
365
|
+
utcSec -= Math.trunc(tzOffsetMinutes) * 60;
|
|
366
|
+
}
|
|
367
|
+
return utcSec * 1000;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Parse Pine / Python date strings (month names, ISO, optional GMT/UTC offset). */
|
|
371
|
+
export function parseTimestampString(text: string): number | null {
|
|
372
|
+
const s0 = text.trim();
|
|
373
|
+
if (!s0) return null;
|
|
374
|
+
const sNorm = normalizeTimestampText(s0);
|
|
375
|
+
const direct = tryTimestampFormats(sNorm, 0);
|
|
376
|
+
if (direct != null) return direct;
|
|
377
|
+
const stripped = stripTimezone(sNorm);
|
|
378
|
+
const again = normalizeTimestampText(stripped.date);
|
|
379
|
+
if (!again) return null;
|
|
380
|
+
return tryTimestampFormats(again, stripped.offsetMin);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Pine `timestamp` dispatcher: date string, year/month/day[ /h/mi/s], or timezone-first.
|
|
385
|
+
* Unknown timezone names are UTC (offset 0).
|
|
386
|
+
*/
|
|
387
|
+
export function timestamp(...args: unknown[]): number | null {
|
|
388
|
+
const n = args.length;
|
|
389
|
+
if (n === 0) return null;
|
|
390
|
+
|
|
391
|
+
if (n === 1) {
|
|
392
|
+
const a = args[0];
|
|
393
|
+
if (typeof a === "string") return parseTimestampString(a);
|
|
394
|
+
const c = requiredTrunc(a);
|
|
395
|
+
return c;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (typeof args[0] === "string") {
|
|
399
|
+
const stripped = args[0].trim();
|
|
400
|
+
const isNumericStr = stripped !== "" && /^[+-]?\d+(?:\.\d+)?$/.test(stripped);
|
|
401
|
+
if (n >= 4 && !isNumericStr) {
|
|
402
|
+
return timestampFromComponents(
|
|
403
|
+
args[1] as number,
|
|
404
|
+
args[2] as number,
|
|
405
|
+
args[3] as number,
|
|
406
|
+
n > 4 ? (args[4] as number) : 0,
|
|
407
|
+
n > 5 ? (args[5] as number) : 0,
|
|
408
|
+
n > 6 ? (args[6] as number) : 0,
|
|
409
|
+
parseTimezoneOffsetMinutes(args[0]),
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
if (n < 4) return parseTimestampString(args[0]);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (n >= 3) {
|
|
416
|
+
return timestampFromComponents(
|
|
417
|
+
args[0] as number,
|
|
418
|
+
args[1] as number,
|
|
419
|
+
args[2] as number,
|
|
420
|
+
n > 3 ? (args[3] as number) : 0,
|
|
421
|
+
n > 4 ? (args[4] as number) : 0,
|
|
422
|
+
n > 5 ? (args[5] as number) : 0,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** ISO week number (`datetime.isocalendar()[1]`). Non-finite → `null`. */
|
|
429
|
+
export function weekOfYear(ms: number): number | null {
|
|
430
|
+
if (!Number.isFinite(ms)) return null;
|
|
431
|
+
const p = utcPartsFromMs(ms);
|
|
432
|
+
return isoWeekOfYear(p.year, p.month, p.dayofmonth);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Midnight UTC of the calendar day that contains `ms`. Non-finite → `null`. */
|
|
436
|
+
export function timeTradingDay(ms: number): number | null {
|
|
437
|
+
if (!Number.isFinite(ms)) return null;
|
|
438
|
+
return Math.floor(ms / MS_PER_DAY) * MS_PER_DAY;
|
|
439
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2024-2026 jango_blockchained
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*
|
|
5
|
+
* Pine `timeframe.*` conversion helpers. Token parse via `timeframeMinutes`
|
|
6
|
+
* (same 30-day month as `request.ts`). na / empty / unknown → null.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { timeframeMinutes } from "./request.ts";
|
|
10
|
+
|
|
11
|
+
const SECONDS_PER_MINUTE = 60;
|
|
12
|
+
const SECONDS_PER_DAY = 86_400;
|
|
13
|
+
const SECONDS_PER_WEEK = 604_800;
|
|
14
|
+
const SECONDS_PER_MONTH = 2_592_000; // 30-day month
|
|
15
|
+
|
|
16
|
+
/** `"1"`/`"5"`/`"60"`/`"1H"`/`"D"`/`"1W"`/`"1M"` → seconds; unknown / empty / na → null. */
|
|
17
|
+
export function timeframeInSeconds(period: string | null): number | null {
|
|
18
|
+
const minutes = timeframeMinutes(period);
|
|
19
|
+
return minutes == null ? null : minutes * SECONDS_PER_MINUTE;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Compact token whose duration is the closest that does not go under `seconds`.
|
|
24
|
+
* `"1"`, `"5"`, `"60"`, `"D"`, `"W"`, `"M"` (and `ND` / `NW` / `NM` when n > 1).
|
|
25
|
+
*/
|
|
26
|
+
export function timeframeFromSeconds(seconds: number): string | null {
|
|
27
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return null;
|
|
28
|
+
if (seconds <= SECONDS_PER_MINUTE) return "1";
|
|
29
|
+
if (seconds <= SECONDS_PER_DAY) {
|
|
30
|
+
const minutes = Math.ceil(seconds / SECONDS_PER_MINUTE);
|
|
31
|
+
return minutes >= SECONDS_PER_DAY / SECONDS_PER_MINUTE ? "D" : String(minutes);
|
|
32
|
+
}
|
|
33
|
+
if (seconds <= SECONDS_PER_WEEK) {
|
|
34
|
+
const days = Math.ceil(seconds / SECONDS_PER_DAY);
|
|
35
|
+
if (days >= 7) return "W";
|
|
36
|
+
return days <= 1 ? "D" : `${days}D`;
|
|
37
|
+
}
|
|
38
|
+
if (seconds <= SECONDS_PER_MONTH) {
|
|
39
|
+
const weeks = Math.ceil(seconds / SECONDS_PER_WEEK);
|
|
40
|
+
if (weeks * SECONDS_PER_WEEK >= SECONDS_PER_MONTH) return "M";
|
|
41
|
+
return weeks <= 1 ? "W" : `${weeks}W`;
|
|
42
|
+
}
|
|
43
|
+
const months = Math.ceil(seconds / SECONDS_PER_MONTH);
|
|
44
|
+
return months <= 1 ? "M" : `${months}M`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Seconds / minutes / hours — not D/W/M. Unknown / empty / na → false. */
|
|
48
|
+
export function timeframeIsIntraday(period: string | null): boolean {
|
|
49
|
+
if (timeframeIsDaily(period) || timeframeIsWeekly(period) || timeframeIsMonthly(period)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return timeframeMinutes(period) != null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function timeframeIsDaily(period: string | null): boolean {
|
|
56
|
+
const p = norm(period);
|
|
57
|
+
if (p == null || /^\d+$/.test(p)) return false;
|
|
58
|
+
return p === "D" || p === "DAY" || p === "DAYS" || /^\d+D$/.test(p);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function timeframeIsWeekly(period: string | null): boolean {
|
|
62
|
+
const p = norm(period);
|
|
63
|
+
if (p == null || /^\d+$/.test(p)) return false;
|
|
64
|
+
return p === "W" || p === "WEEK" || p === "WEEKS" || /^\d+W$/.test(p);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `"M"` / `"1M"` / `"MO"` / `"nMO"` are monthly; `"15M"` is minutes. */
|
|
68
|
+
export function timeframeIsMonthly(period: string | null): boolean {
|
|
69
|
+
const p = norm(period);
|
|
70
|
+
if (p == null || /^\d+$/.test(p)) return false;
|
|
71
|
+
return p === "M" || p === "1M" || p === "MO" || p === "MONTH" || p === "MONTHS" || /^\d+MO$/.test(p);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Leading integer, or 1 when missing / empty / na. */
|
|
75
|
+
export function timeframeMultiplier(period: string | null): number {
|
|
76
|
+
if (period == null || period === "") return 1;
|
|
77
|
+
const m = period.trim().match(/^(\d+)/);
|
|
78
|
+
return m ? Number(m[1]) : 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function norm(period: string | null): string | null {
|
|
82
|
+
if (period == null) return null;
|
|
83
|
+
const p = period.trim().toUpperCase();
|
|
84
|
+
return p === "" ? null : p;
|
|
85
|
+
}
|