@mathieuc/tradingview 4.0.0-beta.4 → 4.0.0-beta.5

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.
@@ -150,3 +150,12 @@ resolved by a successful HTTP scan. Watchlists and paid/private cases remain ope
150
150
  Beta.4 adds read-only `getWatchlists` and scanner-ranked `getHotlist` (#87).
151
151
  Authenticated discovery returned two empty lists; populated entries are fixture-tested.
152
152
  Exact TradingView UI hotlist parity is not claimed. See [watchlists](watchlists.md).
153
+
154
+ ## Account and endurance follow-up
155
+
156
+ Beta.5 ships opt-in account/endurance diagnostics and documents six successful
157
+ private USER-script runs. Deep Backtesting reached the server but returned
158
+ `request_error` / `not_allowed` with the available account. This is a concrete
159
+ prerequisite blocker, not implemented Premium support. Original private-script,
160
+ CME/GUI and 12-hour issue fixtures remain necessary; no historical issue is closed
161
+ from the broader smoke results. See [reliability evidence](v4-reliability.md).
@@ -34,3 +34,61 @@ V4 now starts the account lookup at `/chart/` and follows `Location` only on HTT
34
34
  - Basic daily Replay and strategy reports are documented in [v4-coverage.md](v4-coverage.md); this is not proof of Deep Backtesting or universal intraday entitlement.
35
35
  - Private/invite-only script cases, subscribed futures parity, owned layouts and long-running Pine failures need matching assets and targeted reproductions. See [backlog triage](v4-backlog-triage.md).
36
36
  - Live CI is non-blocking. Review its actual test results, not just the overall workflow conclusion, before release.
37
+
38
+ ## Account-scoped diagnostics (beta.5)
39
+
40
+ From a source checkout, set `SESSION` and `SIGNATURE` securely and run
41
+ `npm run probe:account`. The read-only probe authenticates, lists saved scripts,
42
+ then evaluates at most ten scripts sequentially on 100 daily BTCUSDT bars. Use
43
+ `--max-scripts=N` and `--timeout-ms=N` after `--` to change these bounds.
44
+ It prints only counts, index-based outcomes, script namespace categories and the
45
+ account token's plan claim. It does not print script IDs, names, source, values,
46
+ account identifiers or credentials. A plan claim is informational, not proof of
47
+ exchange or Deep Backtesting entitlement. Failures exit nonzero; an empty saved
48
+ script list is not considered successful private-script coverage.
49
+
50
+ The package now includes both diagnostics, so installed consumers can run:
51
+
52
+ ```sh
53
+ node node_modules/@mathieuc/tradingview/scripts/probe-account.mjs
54
+ node node_modules/@mathieuc/tradingview/scripts/endurance.mjs --minutes=7 --cycle-seconds=600 --require-post-threshold-updates
55
+ ```
56
+
57
+ The second command requires candle **and** quote callbacks after more than 30
58
+ heartbeats on the same connection. `--heartbeat-threshold=N` changes the threshold.
59
+ Counters reset per connection for threshold eligibility; ten short connections do
60
+ not constitute one long connection. Initial snapshots alone cannot satisfy this
61
+ mode. Any recorded error or interruption also makes the probe fail. Without the
62
+ flag, a completed run remains a transport/lifecycle check, not a liveness guarantee.
63
+ Run strict mode on an open market; a closed venue cannot supply the required proof.
64
+
65
+ ### Verified private scripts and outstanding paid-account prerequisite
66
+
67
+ On 3 October 2026, all six saved `USER;` scripts accessible to the available account
68
+ completed: two studies and four strategies, each returning 100 candles and 200
69
+ study rows. Three strategies returned 208, 4 and 69 trades respectively; the fourth
70
+ returned a report with zero trades. These are account-specific fixtures, not a
71
+ reproduction of the private strategy in #89 or the table payload in #251.
72
+
73
+ A bounded request to `history-data` using the protocol in PR #204 reached the server
74
+ and returned `request_error` / `not_allowed`. No Deep Backtesting success is claimed.
75
+ An eligible account and current successful protocol capture are required before
76
+ shipping that API. No subscription was purchased and no account content was modified.
77
+ The original scripts/fixtures for #188 (long-running Pine depth error), #24 (Renko
78
+ GUI parity) and #311 (subscribed CME sessions) remain unavailable for exact reproduction.
79
+
80
+ ### Unbroken-socket checks, 3 October 2026
81
+
82
+ - **BTCUSDT, six minutes, one connection:** 35 heartbeats, 85 candle callbacks,
83
+ 89 quote callbacks; after heartbeat 30 on that same connection, 13 candle and
84
+ 12 quote callbacks. Zero errors/failures; strict mode exited successfully.
85
+ - **OANDA:EURUSD HeikinAshi, seven minutes, one connection:** 41 heartbeats,
86
+ one initial candle callback and one initial quote callback, zero errors/failures.
87
+ The forex market was closed: this proves heartbeat continuity only and does not
88
+ resolve #236's reported price-stream freeze on an open market.
89
+ - Negative controls: a short initial-snapshot-only run fails strict mode, and an
90
+ interrupted run exits nonzero after cleanup. Account diagnostics also fail cleanly
91
+ without credentials; a bounded one-script run was separately verified on Bun.
92
+
93
+ Neither run is a 12-hour Pine reproduction. The full authenticated live suite on
94
+ beta.4 passed all 22 tests; beta.5 changes diagnostic tooling/documentation only.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mathieuc/tradingview",
3
- "version": "4.0.0-beta.4",
3
+ "version": "4.0.0-beta.5",
4
4
  "description": "TradingView market data for JavaScript and TypeScript: candles, quotes, indicators and strategies, with a simple data API and full low-level access.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,7 +21,9 @@
21
21
  "files": [
22
22
  "dist",
23
23
  "README.md",
24
- "docs"
24
+ "docs",
25
+ "scripts/endurance.mjs",
26
+ "scripts/probe-account.mjs"
25
27
  ],
26
28
  "sideEffects": false,
27
29
  "scripts": {
@@ -36,7 +38,8 @@
36
38
  "check": "npm run typecheck && npm run lint && npm test && npm run build && npm run smoke",
37
39
  "prepack": "npm run build",
38
40
  "probe:endurance": "npm run build && node scripts/endurance.mjs",
39
- "probe:parity": "npm run build && node scripts/parity.mjs"
41
+ "probe:parity": "npm run build && node scripts/parity.mjs",
42
+ "probe:account": "npm run build && node scripts/probe-account.mjs"
40
43
  },
41
44
  "repository": {
42
45
  "type": "git",
@@ -0,0 +1,93 @@
1
+ /** Opt-in live endurance probe. No credentials are read or logged. */
2
+ import { TradingViewClient } from '../dist/index.js';
3
+ import { watchCandles, watchQuotes } from '../dist/data/index.js';
4
+
5
+ const argument = (name, fallback) => {
6
+ const raw = process.argv.find((value) => value.startsWith(`--${name}=`))?.split('=')[1];
7
+ const number = raw === undefined ? fallback : Number(raw);
8
+ if (!Number.isFinite(number) || number <= 0) throw new Error(`--${name} must be positive`);
9
+ return number;
10
+ };
11
+
12
+ const textArgument = (name, fallback) => process.argv.find((value) => value.startsWith(`--${name}=`))?.split('=')[1] ?? fallback;
13
+ const minutes = argument('minutes', 120);
14
+ const cycleSeconds = argument('cycle-seconds', 600);
15
+ const heartbeatThreshold = argument('heartbeat-threshold', 30);
16
+ const requirePostThresholdUpdates = process.argv.includes('--require-post-threshold-updates');
17
+ const symbol = textArgument('symbol', 'BINANCE:BTCUSDT');
18
+ const timeframe = textArgument('timeframe', '1');
19
+ const chartType = textArgument('chart-type', undefined);
20
+ const deadline = Date.now() + minutes * 60_000;
21
+ const metrics = { cycles: 0, starts: 0, failures: 0, candleUpdates: 0, quoteUpdates: 0, heartbeats: 0, postThresholdCandleUpdates: 0, postThresholdQuoteUpdates: 0, errors: [] };
22
+ let interrupted = false;
23
+ for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => { interrupted = true; });
24
+
25
+ const report = (event, extra = {}) => console.log(JSON.stringify({
26
+ at: new Date().toISOString(), event, ...extra,
27
+ }));
28
+
29
+ function waitForCycle(ms, watchers) {
30
+ return new Promise((resolve) => {
31
+ const timer = setTimeout(() => resolve('elapsed'), ms);
32
+ Promise.race(watchers.map((watcher) => watcher.closed)).then(() => {
33
+ clearTimeout(timer);
34
+ resolve('closed');
35
+ });
36
+ });
37
+ }
38
+
39
+ const status = setInterval(() => report('status', { ...metrics, errors: metrics.errors.slice(-3) }), 60_000);
40
+ report('start', { minutes, cycleSeconds, symbol, timeframe, chartType, heartbeatThreshold, requirePostThresholdUpdates });
41
+ try {
42
+ while (!interrupted && Date.now() < deadline) {
43
+ metrics.cycles += 1;
44
+ const client = new TradingViewClient();
45
+ let cycleHeartbeats = 0;
46
+ client.on('heartbeat', () => { metrics.heartbeats += 1; cycleHeartbeats += 1; });
47
+ client.on('error', (error) => metrics.errors.push(error.code));
48
+ let candles;
49
+ let quotes;
50
+ try {
51
+ candles = await watchCandles({ symbol, timeframe, chartType, count: 5, client }, {
52
+ onData: () => {
53
+ metrics.candleUpdates += 1;
54
+ if (cycleHeartbeats > heartbeatThreshold) metrics.postThresholdCandleUpdates += 1;
55
+ },
56
+ onError: (error) => metrics.errors.push(error.code),
57
+ });
58
+ quotes = await watchQuotes({ symbols: [symbol], fields: 'price', client }, {
59
+ onData: () => {
60
+ metrics.quoteUpdates += 1;
61
+ if (cycleHeartbeats > heartbeatThreshold) metrics.postThresholdQuoteUpdates += 1;
62
+ },
63
+ onError: (error) => metrics.errors.push(error.code),
64
+ });
65
+ if (candles.latest.length !== 5 || typeof quotes.latest[symbol]?.lp !== 'number') {
66
+ throw new Error('Initial market snapshots incomplete');
67
+ }
68
+ metrics.starts += 1;
69
+ report('connected', { cycle: metrics.cycles, candles: candles.latest.length, quotePricePresent: true });
70
+ const duration = Math.min(cycleSeconds * 1000, deadline - Date.now());
71
+ const outcome = await waitForCycle(duration, [candles, quotes]);
72
+ if (outcome === 'closed' && !interrupted) throw new Error('Watcher closed before planned rotation');
73
+ report('rotation', { cycle: metrics.cycles, outcome });
74
+ } catch (error) {
75
+ metrics.failures += 1;
76
+ metrics.errors.push(error?.code ?? error?.message ?? String(error));
77
+ report('failure', { cycle: metrics.cycles, code: error?.code, message: error?.message });
78
+ } finally {
79
+ await Promise.allSettled([candles?.stop(), quotes?.stop()]);
80
+ await client.close();
81
+ }
82
+ if (metrics.failures >= 3) break;
83
+ if (metrics.failures && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 3_000));
84
+ }
85
+ } finally {
86
+ clearInterval(status);
87
+ report('summary', { ...metrics, errors: metrics.errors.slice(-10), interrupted });
88
+ }
89
+ if (interrupted || metrics.failures || metrics.errors.length || metrics.starts === 0
90
+ || metrics.quoteUpdates === 0 || metrics.candleUpdates === 0
91
+ || (requirePostThresholdUpdates && (!metrics.postThresholdCandleUpdates || !metrics.postThresholdQuoteUpdates))) {
92
+ process.exitCode = 1;
93
+ }
@@ -0,0 +1,55 @@
1
+ /** Opt-in read-only diagnostics. Never print account tokens, script IDs or names. */
2
+ import { Buffer } from 'node:buffer';
3
+ import { getUser, getPrivateIndicators, getIndicatorData } from '../dist/index.js';
4
+
5
+ const positiveArgument = (name, fallback) => {
6
+ const raw = process.argv.find((value) => value.startsWith(`--${name}=`))?.slice(name.length + 3);
7
+ const value = raw === undefined ? fallback : Number(raw);
8
+ if (!Number.isSafeInteger(value) || value < 1) throw new Error('Invalid probe argument');
9
+ return value;
10
+ };
11
+
12
+ async function main() {
13
+ const maxScripts = positiveArgument('max-scripts', 10);
14
+ const timeoutMs = positiveArgument('timeout-ms', 30_000);
15
+ if (!process.env.SESSION) throw new Error('SESSION is required');
16
+ const credentials = { session: process.env.SESSION, signature: process.env.SIGNATURE };
17
+ const options = { signal: AbortSignal.timeout(timeoutMs) };
18
+ const user = await getUser(credentials, options);
19
+ let planClaim = null;
20
+ try {
21
+ const claims = JSON.parse(Buffer.from(user.authToken.split('.')[1], 'base64url').toString());
22
+ if (typeof claims.plan === 'string') planClaim = claims.plan;
23
+ } catch { /* The account token is not guaranteed to remain a JWT. */ }
24
+ console.log(JSON.stringify({ event: 'account', authenticated: true, planClaim }));
25
+ const scripts = await getPrivateIndicators(credentials, { signal: AbortSignal.timeout(timeoutMs) });
26
+ let failures = 0;
27
+ const selected = scripts.slice(0, maxScripts);
28
+ console.log(JSON.stringify({ event: 'selection', available: scripts.length, selected: selected.length }));
29
+ for (const [index, script] of selected.entries()) {
30
+ const classification = script.id.startsWith('USER;') ? 'user' : script.id.startsWith('PUB;') ? 'published' : 'other';
31
+ try {
32
+ const result = await getIndicatorData({
33
+ symbol: 'BINANCE:BTCUSDT', timeframe: '1D', count: 100,
34
+ indicator: script.id, credentials, timeoutMs, signal: AbortSignal.timeout(timeoutMs),
35
+ });
36
+ console.log(JSON.stringify({
37
+ event: 'script', index, classification, ok: true,
38
+ candles: result.candles.length, values: result.values.length,
39
+ trades: result.strategyReport.trades?.length ?? 0,
40
+ reportPresent: Object.keys(result.strategyReport.performance ?? {}).length > 0,
41
+ }));
42
+ } catch (error) {
43
+ failures += 1;
44
+ console.log(JSON.stringify({ event: 'script', index, classification, ok: false, code: error?.code ?? 'ERROR' }));
45
+ }
46
+ }
47
+ console.log(JSON.stringify({ event: 'summary', checked: selected.length, failures, remaining: scripts.length - selected.length }));
48
+ if (failures || selected.length === 0) process.exitCode = 1;
49
+ }
50
+
51
+ main().catch((error) => {
52
+ // Do not log message/details: upstream failures may carry private script payloads.
53
+ console.error(JSON.stringify({ event: 'failure', code: error?.code ?? 'CONFIG_OR_NETWORK_ERROR' }));
54
+ process.exitCode = 1;
55
+ });