@mathieuc/tradingview 4.0.0-beta.3 → 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.
- package/dist/data/index.d.ts +2 -0
- package/dist/data/index.js +1 -0
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +1 -0
- package/dist/http/watchlists.d.ts +22 -0
- package/dist/http/watchlists.js +42 -0
- package/docs/data-api.md +5 -0
- package/docs/v4-backlog-triage.md +15 -0
- package/docs/v4-reliability.md +58 -0
- package/docs/watchlists.md +56 -0
- package/package.json +6 -3
- package/scripts/endurance.mjs +93 -0
- package/scripts/probe-account.mjs +55 -0
package/dist/data/index.d.ts
CHANGED
|
@@ -30,3 +30,5 @@ export { summarizeStrategyReport } from '../chart/strategy.js';
|
|
|
30
30
|
export type { StrategySummary } from '../chart/strategy.js';
|
|
31
31
|
export { getScreener } from '../http/screener.js';
|
|
32
32
|
export type { ScreenerFilter, ScreenerQuery, ScreenerResult, ScreenerRow } from '../http/screener.js';
|
|
33
|
+
export { getWatchlists, getHotlist } from '../http/watchlists.js';
|
|
34
|
+
export type { Watchlist, WatchlistOptions, HotlistKind, HotlistQuery } from '../http/watchlists.js';
|
package/dist/data/index.js
CHANGED
|
@@ -15,3 +15,4 @@ export { getTechnicalAnalysis, searchIndicators, searchMarkets, } from '../http/
|
|
|
15
15
|
export { TradingViewError } from '../errors.js';
|
|
16
16
|
export { summarizeStrategyReport } from '../chart/strategy.js';
|
|
17
17
|
export { getScreener } from '../http/screener.js';
|
|
18
|
+
export { getWatchlists, getHotlist } from '../http/watchlists.js';
|
package/dist/http/index.d.ts
CHANGED
|
@@ -11,3 +11,5 @@ export { PinePermissionManager } from './pine-permissions.js';
|
|
|
11
11
|
export type { AuthorizedUser, AuthorizedUserOrder, PinePermissionOptions } from './pine-permissions.js';
|
|
12
12
|
export { getScreener } from './screener.js';
|
|
13
13
|
export type { ScreenerFilter, ScreenerQuery, ScreenerResult, ScreenerRow } from './screener.js';
|
|
14
|
+
export { getWatchlists, getHotlist } from './watchlists.js';
|
|
15
|
+
export type { Watchlist, WatchlistOptions, HotlistKind, HotlistQuery } from './watchlists.js';
|
package/dist/http/index.js
CHANGED
|
@@ -4,3 +4,4 @@ export { clearIndicatorCache, getIndicator, getPrivateIndicators, parseIndicator
|
|
|
4
4
|
export { getChartToken, getDrawings } from './layouts.js';
|
|
5
5
|
export { PinePermissionManager } from './pine-permissions.js';
|
|
6
6
|
export { getScreener } from './screener.js';
|
|
7
|
+
export { getWatchlists, getHotlist } from './watchlists.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Credentials, type HttpOptions } from './request.js';
|
|
2
|
+
import { type ScreenerQuery, type ScreenerResult } from './screener.js';
|
|
3
|
+
export interface Watchlist {
|
|
4
|
+
id: number;
|
|
5
|
+
name: string;
|
|
6
|
+
/** Ordered upstream entries, including any section markers. */
|
|
7
|
+
symbols: string[];
|
|
8
|
+
/** Additional upstream metadata, preserved without coercion. */
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
export interface WatchlistOptions extends HttpOptions {
|
|
12
|
+
credentials: Credentials;
|
|
13
|
+
}
|
|
14
|
+
/** Reads all watchlists belonging to the authenticated account. Never modifies them. */
|
|
15
|
+
export declare function getWatchlists(options: WatchlistOptions): Promise<Watchlist[]>;
|
|
16
|
+
export type HotlistKind = 'gainers' | 'losers' | 'mostActive' | 'volumeGainers';
|
|
17
|
+
export interface HotlistQuery extends Omit<ScreenerQuery, 'columns' | 'sort'> {
|
|
18
|
+
kind: HotlistKind;
|
|
19
|
+
columns?: string[];
|
|
20
|
+
}
|
|
21
|
+
/** Scanner-ranked lists, not a promise of parity with the TradingView UI hotlist widget. */
|
|
22
|
+
export declare function getHotlist(query: HotlistQuery, options?: HttpOptions): Promise<ScreenerResult>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { TradingViewError } from '../errors.js';
|
|
2
|
+
import { request } from './request.js';
|
|
3
|
+
import { getScreener } from './screener.js';
|
|
4
|
+
/** Reads all watchlists belonging to the authenticated account. Never modifies them. */
|
|
5
|
+
export async function getWatchlists(options) {
|
|
6
|
+
if (!options?.credentials?.session) {
|
|
7
|
+
throw new TradingViewError('INVALID_ARGUMENT', 'getWatchlists requires account credentials');
|
|
8
|
+
}
|
|
9
|
+
const { data, status } = await request('https://www.tradingview.com/api/v1/symbols_list/all/', {
|
|
10
|
+
credentials: options.credentials,
|
|
11
|
+
headers: { origin: 'https://www.tradingview.com' },
|
|
12
|
+
redirect: 'manual',
|
|
13
|
+
}, options);
|
|
14
|
+
if (status === 401 || status === 403 || (status >= 300 && status < 400)) {
|
|
15
|
+
throw new TradingViewError('AUTH_ERROR', `Watchlist access rejected (HTTP ${status})`);
|
|
16
|
+
}
|
|
17
|
+
if (status < 200 || status >= 300 || !Array.isArray(data)
|
|
18
|
+
|| data.some((list) => !list || !Number.isSafeInteger(list.id) || typeof list.name !== 'string'
|
|
19
|
+
|| !Array.isArray(list.symbols) || list.symbols.some((symbol) => typeof symbol !== 'string'))) {
|
|
20
|
+
throw new TradingViewError('HTTP_ERROR', `Unexpected watchlist response (HTTP ${status})`);
|
|
21
|
+
}
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
24
|
+
/** Scanner-ranked lists, not a promise of parity with the TradingView UI hotlist widget. */
|
|
25
|
+
export async function getHotlist(query, options = {}) {
|
|
26
|
+
const rankings = {
|
|
27
|
+
gainers: { sortBy: 'change', sortOrder: 'desc' },
|
|
28
|
+
losers: { sortBy: 'change', sortOrder: 'asc' },
|
|
29
|
+
mostActive: { sortBy: 'volume', sortOrder: 'desc' },
|
|
30
|
+
volumeGainers: { sortBy: 'relative_volume_10d_calc', sortOrder: 'desc' },
|
|
31
|
+
};
|
|
32
|
+
if (!Object.hasOwn(rankings, query.kind)) {
|
|
33
|
+
throw new TradingViewError('INVALID_ARGUMENT', 'Unknown hotlist kind');
|
|
34
|
+
}
|
|
35
|
+
return getScreener({
|
|
36
|
+
...query,
|
|
37
|
+
market: query.market ?? 'america',
|
|
38
|
+
columns: query.columns ?? ['name', 'close', 'change', 'volume', 'relative_volume_10d_calc'],
|
|
39
|
+
filter: query.filter ?? [{ left: 'type', operation: 'equal', right: 'stock' }],
|
|
40
|
+
sort: rankings[query.kind],
|
|
41
|
+
}, options);
|
|
42
|
+
}
|
package/docs/data-api.md
CHANGED
|
@@ -289,3 +289,8 @@ Every failure is a `TradingViewError` with a `code`:
|
|
|
289
289
|
Use [`getScreener`](screener.md) for a single scanner page with custom columns,
|
|
290
290
|
filters, ranking and explicit pagination. Available from this high-level entry
|
|
291
291
|
point as well as the root package; no chart or quote session is required.
|
|
292
|
+
|
|
293
|
+
## Watchlists and rankings
|
|
294
|
+
|
|
295
|
+
[`getWatchlists` and `getHotlist`](watchlists.md) provide read-only account lists
|
|
296
|
+
and scanner-based gainers, losers and volume rankings. Available from both entry points.
|
|
@@ -144,3 +144,18 @@ No issues/legacy PRs were closed, and no contributor comments were sent by this
|
|
|
144
144
|
Beta.3 adds `getScreener` for #53 and scanner fields relevant to #280. See
|
|
145
145
|
[screener documentation](screener.md). #85 real-time exchange entitlement is not
|
|
146
146
|
resolved by a successful HTTP scan. Watchlists and paid/private cases remain open.
|
|
147
|
+
|
|
148
|
+
## Watchlist follow-up
|
|
149
|
+
|
|
150
|
+
Beta.4 adds read-only `getWatchlists` and scanner-ranked `getHotlist` (#87).
|
|
151
|
+
Authenticated discovery returned two empty lists; populated entries are fixture-tested.
|
|
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).
|
package/docs/v4-reliability.md
CHANGED
|
@@ -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.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Watchlists and ranked lists
|
|
2
|
+
|
|
3
|
+
## Read your watchlists
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { getWatchlists } from '@mathieuc/tradingview/data';
|
|
7
|
+
|
|
8
|
+
const lists = await getWatchlists({
|
|
9
|
+
credentials: {
|
|
10
|
+
session: process.env.TV_SESSION,
|
|
11
|
+
signature: process.env.TV_SIGNATURE,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
for (const list of lists) console.log(list.id, list.name, list.symbols);
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This is a read-only account API. Each list preserves its numeric ID, name, ordered
|
|
18
|
+
`symbols` and additional upstream metadata. Entries can include section markers;
|
|
19
|
+
do not blindly pass every entry to `getQuotes`. Filter for exchange-qualified
|
|
20
|
+
symbols appropriate to your application. No lists are created, edited or deleted.
|
|
21
|
+
|
|
22
|
+
Credentials are required. Access rejection (401/403 or login redirects) produces
|
|
23
|
+
`AUTH_ERROR`; malformed responses and other failed statuses produce `HTTP_ERROR`.
|
|
24
|
+
Authenticated redirects are not followed. The options also accept `fetch`,
|
|
25
|
+
`headers` and `signal`. Treat returned names/symbols as private account data.
|
|
26
|
+
|
|
27
|
+
## Scanner-ranked lists
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
import { getHotlist } from '@mathieuc/tradingview/data';
|
|
31
|
+
|
|
32
|
+
const page = await getHotlist({ kind: 'gainers', range: [0, 10] });
|
|
33
|
+
console.log(page.rows);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| Kind | Ranking |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `gainers` | Percentage change descending |
|
|
39
|
+
| `losers` | Percentage change ascending |
|
|
40
|
+
| `mostActive` | Volume descending |
|
|
41
|
+
| `volumeGainers` | Relative volume (`relative_volume_10d_calc`) descending |
|
|
42
|
+
|
|
43
|
+
These are convenient [screener](screener.md) queries, **not an exact replica of the
|
|
44
|
+
TradingView hotlist widget**. UI universe, session and liquidity filters may differ.
|
|
45
|
+
Default universe: America stocks; default fields: name, close, change, volume and
|
|
46
|
+
relative volume. Override `market`, `columns`, `filter`, `range` or `symbols` as
|
|
47
|
+
needed. For a non-stock universe, supply appropriate filters (or `filter: []`),
|
|
48
|
+
since changing the market does not remove the default stock filter. Exchange data
|
|
49
|
+
may be delayed. No automatic polling or paid entitlement is implied.
|
|
50
|
+
|
|
51
|
+
## Evidence and limits
|
|
52
|
+
|
|
53
|
+
On 3 October 2026, the account endpoint returned two watchlists successfully.
|
|
54
|
+
Both were empty, so nonempty ordered entries and section preservation are covered
|
|
55
|
+
by deterministic fixtures, not claimed as live-tested account content. An anonymous
|
|
56
|
+
America relative-volume scan returned populated rows. No account data was modified.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mathieuc/tradingview",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
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
|
+
});
|