oakscriptjs 0.10.0 → 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 +8 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +30 -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/runtime/index.d.ts +2 -1
- package/dist/cjs/runtime/index.d.ts.map +1 -1
- package/dist/cjs/runtime/index.js +2 -0
- package/dist/cjs/runtime/index.js.map +2 -2
- package/dist/cjs/runtime/input-active.d.ts +27 -0
- package/dist/cjs/runtime/input-active.d.ts.map +1 -0
- package/dist/cjs/runtime/input-active.js +44 -0
- package/dist/cjs/runtime/input-active.js.map +7 -0
- package/dist/cjs/runtime/types.d.ts +29 -2
- 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 +53 -17
- package/dist/cjs/script/index.d.ts.map +1 -1
- package/dist/cjs/script/index.js +70 -11
- 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 +12 -4
- package/dist/cjs/str/index.d.ts.map +1 -1
- package/dist/cjs/str/index.js +13 -7
- package/dist/cjs/str/index.js.map +2 -2
- package/dist/cjs/str/numberformat.d.ts +10 -2
- package/dist/cjs/str/numberformat.d.ts.map +1 -1
- package/dist/cjs/str/numberformat.js +10 -2
- 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 +8 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +11 -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/runtime/index.d.ts +2 -1
- package/dist/esm/runtime/index.d.ts.map +1 -1
- package/dist/esm/runtime/index.js +1 -0
- package/dist/esm/runtime/index.js.map +1 -1
- package/dist/esm/runtime/input-active.d.ts +27 -0
- package/dist/esm/runtime/input-active.d.ts.map +1 -0
- package/dist/esm/runtime/input-active.js +54 -0
- package/dist/esm/runtime/input-active.js.map +1 -0
- package/dist/esm/runtime/types.d.ts +29 -2
- package/dist/esm/runtime/types.d.ts.map +1 -1
- package/dist/esm/script/index.d.ts +53 -17
- package/dist/esm/script/index.d.ts.map +1 -1
- package/dist/esm/script/index.js +80 -13
- 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 +12 -4
- package/dist/esm/str/index.d.ts.map +1 -1
- package/dist/esm/str/index.js +33 -12
- package/dist/esm/str/index.js.map +1 -1
- package/dist/esm/str/numberformat.d.ts +10 -2
- package/dist/esm/str/numberformat.d.ts.map +1 -1
- package/dist/esm/str/numberformat.js +16 -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 +36 -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/runtime/index.ts +3 -0
- package/src/runtime/input-active.ts +50 -0
- package/src/runtime/types.ts +25 -2
- package/src/script/index.ts +123 -23
- 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 +30 -12
- package/src/str/numberformat.ts +18 -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
|
@@ -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
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
* @version 6
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { formatNumber } from './numberformat.js';
|
|
12
|
+
import { formatNumber, tickScale } from './numberformat.js';
|
|
13
13
|
import { formatMessage } from './messageformat.js';
|
|
14
14
|
import { formatDate } from './dateformat.js';
|
|
15
|
-
import { round_to_mintick } from '../math/index.js';
|
|
16
15
|
import type { int, bool, float, simple_int, simple_string } from '../types/index.js';
|
|
17
16
|
|
|
18
17
|
/**
|
|
@@ -40,11 +39,15 @@ export function length(str: simple_string): int {
|
|
|
40
39
|
* `format.price` / `format.inherit`
|
|
41
40
|
* @param mintick - Tick size for `format.mintick` (PineScript `syminfo.mintick`, implicit there). Not part of the
|
|
42
41
|
* PineScript signature: the core function cannot see the chart, so pass it, as for `math.round_to_mintick`
|
|
42
|
+
* @param pricescale - Price scale of the symbol for `format.mintick` (PineScript `syminfo.pricescale`), needed for a
|
|
43
|
+
* symbol priced in fractions (32 for a 1/32 tick). Default: 10^(decimals of `mintick`)
|
|
43
44
|
* @returns String representation of the value
|
|
44
45
|
*
|
|
45
46
|
* @remarks
|
|
46
47
|
* As in PineScript:
|
|
47
|
-
* - 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"
|
|
48
51
|
* - patterns round half away from zero on the shortest decimal form: 1.005 with "#.##" gives "1.01";
|
|
49
52
|
* at most 16 decimals from 1e-3 up (0.0012345678901234567 with 20 '#' decimals gives "0.0012345678901235")
|
|
50
53
|
* - "pos;neg" patterns: a negative number takes the text of the negative subpattern ("#.##;(#.##)": "(3.5)")
|
|
@@ -53,8 +56,12 @@ export function length(str: simple_string): int {
|
|
|
53
56
|
* - `format.percent` shows 2 decimals and "%" without multiplying; `format.volume` uses K, M, B, T
|
|
54
57
|
* - `format.price` and `format.inherit` give "price1" / "inherit1" in PineScript: they are patterns
|
|
55
58
|
* without digit characters, and oakscriptjs does the same
|
|
56
|
-
* - `format.mintick`
|
|
57
|
-
*
|
|
59
|
+
* - `format.mintick` shows floor(log10(pricescale)) decimals, zeros included ("1.00", "0.40" for tick 0.01), and
|
|
60
|
+
* rounds in two steps, with mintick = minmove / pricescale: the double quotient `|x| / (1 / pricescale)` half away
|
|
61
|
+
* from zero (140.355 / 0.01 = 14035.499999999998 gives "140.35", where `math.round_to_mintick` gives 140.36),
|
|
62
|
+
* then `floor(units / minmove + 0.5)` ticks (tick 0.02: 0.005 gives "0.02", -0.01 gives "0.00"). With a
|
|
63
|
+
* pricescale that is no power of 10 the tick value is then rounded half even to those decimals (1/32: 25.25 gives
|
|
64
|
+
* "25.2", -0.02 gives "-0.0"). It throws without `mintick`
|
|
58
65
|
* - na gives "NaN" ("NaN%" with `format.percent`); an infinity gives "NaN" too ("NaN%" for `format.percent`,
|
|
59
66
|
* "NaNT" for `format.volume`), except `format.mintick`, which gives "Infinity" / "-Infinity"
|
|
60
67
|
*
|
|
@@ -66,7 +73,9 @@ export function length(str: simple_string): int {
|
|
|
66
73
|
* str.tostring(12345.678, format.volume) // "12.346K"
|
|
67
74
|
* ```
|
|
68
75
|
*/
|
|
69
|
-
export function tostring(value: any, format?: simple_string, mintick?: float): string {
|
|
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(', ') + ']';
|
|
70
79
|
if (typeof value !== 'number') return String(value);
|
|
71
80
|
if (Number.isNaN(value)) return format === 'percent' ? 'NaN%' : 'NaN';
|
|
72
81
|
if (!Number.isFinite(value)) {
|
|
@@ -78,7 +87,8 @@ export function tostring(value: any, format?: simple_string, mintick?: float): s
|
|
|
78
87
|
if (Math.abs(value) >= 1e21 && Number.isFinite(value)) {
|
|
79
88
|
return String(value).replace('e+', 'E').replace(/\.0+E/, 'E');
|
|
80
89
|
}
|
|
81
|
-
|
|
90
|
+
// without a format the exact value is rounded (82325.10000000005 is 82325.10000000004947...: "82325.1")
|
|
91
|
+
return formatNumber(value, '#.##########', 'halfUpExact');
|
|
82
92
|
}
|
|
83
93
|
switch (format) {
|
|
84
94
|
case 'percent':
|
|
@@ -94,11 +104,19 @@ export function tostring(value: any, format?: simple_string, mintick?: float): s
|
|
|
94
104
|
if (mintick === undefined) {
|
|
95
105
|
throw new Error('str.tostring(x, format.mintick) needs the tick size: pass it as str.tostring(x, format.mintick, syminfo.mintick)');
|
|
96
106
|
}
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
const [minmove, scale] = tickScale(mintick, pricescale);
|
|
108
|
+
const decimals = Math.floor(Math.log10(scale) + 1e-9);
|
|
109
|
+
const mask = decimals > 0 ? '0.' + '0'.repeat(decimals) : '0';
|
|
110
|
+
if (!(minmove >= 1)) return formatNumber(value, mask, 'halfUpShortest');
|
|
111
|
+
// 1. the double quotient |x| / (1 / pricescale), half away from zero (not math.round_to_mintick's exact decimal)
|
|
112
|
+
const scaled = Math.abs(value) / (1 / scale);
|
|
113
|
+
let units = Math.trunc(scaled);
|
|
114
|
+
if (scaled - units >= 0.5) units++;
|
|
115
|
+
// 2. to the tick, a half going up (minmove 2: -0.01 gives "0.00", 0.01 gives "0.02")
|
|
116
|
+
const ticks = Math.floor((value < 0 ? -units : units) / minmove + 0.5);
|
|
117
|
+
// 3. the tick value at the scale's decimals; a negative one keeps its sign when it prints as zero ("-0.0")
|
|
118
|
+
const text = formatNumber((Math.abs(ticks) * minmove) / scale, mask, 'halfEvenExact');
|
|
119
|
+
return ticks < 0 ? '-' + text : text;
|
|
102
120
|
}
|
|
103
121
|
default:
|
|
104
122
|
return formatNumber(value, format, 'halfUpShortest');
|
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;
|
|
@@ -230,3 +232,16 @@ function affixes(p: Pattern, negative: boolean, body: string): string {
|
|
|
230
232
|
if (p.negPrefix === null) return '-' + p.prefix + body + p.suffix;
|
|
231
233
|
return p.negPrefix + body + p.negSuffix!;
|
|
232
234
|
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* `[minmove, pricescale]` of a tick (PineScript `syminfo.mintick = syminfo.minmove / syminfo.pricescale`).
|
|
238
|
+
* Without `pricescale` it is 10^(decimals of the tick): 0.02 is 2 / 100, 0.25 is 25 / 100. A symbol priced in
|
|
239
|
+
* fractions needs its own (1/32 is 1 / 32).
|
|
240
|
+
*/
|
|
241
|
+
export function tickScale(mintick: number, pricescale?: number): [number, number] {
|
|
242
|
+
if (pricescale === undefined) {
|
|
243
|
+
const m = /^\d+(?:\.(\d+))?(?:e-(\d+))?$/.exec(String(mintick));
|
|
244
|
+
pricescale = Number(`1e${m ? (m[1]?.length ?? 0) + Number(m[2] ?? 0) : 0}`);
|
|
245
|
+
}
|
|
246
|
+
return [Math.round(mintick * pricescale), pricescale];
|
|
247
|
+
}
|