@adaptic/utils 0.0.989 → 0.0.991
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/index.cjs +754 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +750 -2
- package/dist/index.mjs.map +1 -1
- package/dist/types/__tests__/atr.test.d.ts +2 -0
- package/dist/types/__tests__/atr.test.d.ts.map +1 -0
- package/dist/types/__tests__/index.test.d.ts +2 -0
- package/dist/types/__tests__/index.test.d.ts.map +1 -0
- package/dist/types/__tests__/risk-metrics.test.d.ts +2 -0
- package/dist/types/__tests__/risk-metrics.test.d.ts.map +1 -0
- package/dist/types/__tests__/strategy-metrics.test.d.ts +2 -0
- package/dist/types/__tests__/strategy-metrics.test.d.ts.map +1 -0
- package/dist/types/__tests__/volatility.test.d.ts +2 -0
- package/dist/types/__tests__/volatility.test.d.ts.map +1 -0
- package/dist/types/atr.d.ts +30 -0
- package/dist/types/atr.d.ts.map +1 -0
- package/dist/types/index.d.ts +41 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/misc-utils.d.ts +37 -0
- package/dist/types/misc-utils.d.ts.map +1 -1
- package/dist/types/risk-metrics.d.ts +58 -0
- package/dist/types/risk-metrics.d.ts.map +1 -0
- package/dist/types/strategy-metrics.d.ts +65 -0
- package/dist/types/strategy-metrics.d.ts.map +1 -0
- package/dist/types/trading-policy/schemas/effective-policy.schema.d.ts +4 -4
- package/dist/types/trading-policy/schemas/execution-prefs.schema.d.ts +4 -4
- package/dist/types/trading-policy/schemas/policy-mutation.schema.d.ts +6 -6
- package/dist/types/volatility.d.ts +65 -0
- package/dist/types/volatility.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -6044,6 +6044,202 @@ async function createLimitOrder(auth, params = {
|
|
|
6044
6044
|
}
|
|
6045
6045
|
|
|
6046
6046
|
// Utility function for debug logging
|
|
6047
|
+
/** Sliding-window size (number of recent outcomes tracked per host). */
|
|
6048
|
+
const CIRCUIT_WINDOW_SIZE = 20;
|
|
6049
|
+
/** Maximum age (ms) of an outcome before it is dropped from the window. */
|
|
6050
|
+
const CIRCUIT_WINDOW_TTL_MS = 30_000;
|
|
6051
|
+
/**
|
|
6052
|
+
* Failure ratio (0..1) at which the breaker trips when the window
|
|
6053
|
+
* is full (i.e. CIRCUIT_WINDOW_SIZE outcomes recorded).
|
|
6054
|
+
*
|
|
6055
|
+
* Tuned to be permissive enough that a few transient failures during
|
|
6056
|
+
* normal operation do not trip the breaker, but tight enough that a
|
|
6057
|
+
* persistent upstream outage (where >= half of recent calls are
|
|
6058
|
+
* failing) is detected quickly. With WINDOW=20 and threshold=0.6 we
|
|
6059
|
+
* need 12 failures within the most recent 20 attempts to trip.
|
|
6060
|
+
*/
|
|
6061
|
+
const CIRCUIT_TRIP_FAILURE_RATIO = 0.6;
|
|
6062
|
+
/**
|
|
6063
|
+
* Minimum number of recent outcomes required before the failure-ratio
|
|
6064
|
+
* gate is even evaluated. Without this guard, a single failure on a
|
|
6065
|
+
* cold host would give a 100 % ratio and trip the breaker on the
|
|
6066
|
+
* second attempt — which would be useless protection while also being
|
|
6067
|
+
* highly destructive.
|
|
6068
|
+
*/
|
|
6069
|
+
const CIRCUIT_MIN_SAMPLES = 8;
|
|
6070
|
+
/**
|
|
6071
|
+
* How long the breaker stays open before transitioning to half-open
|
|
6072
|
+
* (i.e. allowing the next request through as a probe). The next
|
|
6073
|
+
* outcome — success or failure — fully recloses or re-opens the
|
|
6074
|
+
* breaker. 5 s aligns with typical upstream recovery times and is
|
|
6075
|
+
* short enough that the next caller drives the probe.
|
|
6076
|
+
*/
|
|
6077
|
+
const CIRCUIT_OPEN_COOLDOWN_MS = 5_000;
|
|
6078
|
+
/**
|
|
6079
|
+
* Fail-fast latency. We sleep a short, deterministic amount before
|
|
6080
|
+
* rejecting so the upstream caller's outer timeout (commonly 10–25 s)
|
|
6081
|
+
* sees a clean rejection rather than a synchronous reject-storm that
|
|
6082
|
+
* could starve other event-loop work. 100 ms aligns with the user's
|
|
6083
|
+
* G1 analysis target: "cap per-symbol fallback latency at 100 ms
|
|
6084
|
+
* during outages — eliminating the 25 s × N-concurrent-symbols
|
|
6085
|
+
* event-loop hog."
|
|
6086
|
+
*/
|
|
6087
|
+
const CIRCUIT_FAIL_FAST_LATENCY_MS = 100;
|
|
6088
|
+
const circuitStates = new Map();
|
|
6089
|
+
function getCircuitState(host) {
|
|
6090
|
+
let state = circuitStates.get(host);
|
|
6091
|
+
if (!state) {
|
|
6092
|
+
state = {
|
|
6093
|
+
host,
|
|
6094
|
+
recent: [],
|
|
6095
|
+
openedAt: 0,
|
|
6096
|
+
lastTripFailureRatio: 0,
|
|
6097
|
+
};
|
|
6098
|
+
circuitStates.set(host, state);
|
|
6099
|
+
}
|
|
6100
|
+
return state;
|
|
6101
|
+
}
|
|
6102
|
+
function pruneStaleSamples(state, now) {
|
|
6103
|
+
const cutoff = now - CIRCUIT_WINDOW_TTL_MS;
|
|
6104
|
+
// Remove entries older than the TTL. Window is small so a simple
|
|
6105
|
+
// filter is fine — no need for a deque structure.
|
|
6106
|
+
if (state.recent.length === 0)
|
|
6107
|
+
return;
|
|
6108
|
+
let firstFreshIndex = 0;
|
|
6109
|
+
while (firstFreshIndex < state.recent.length &&
|
|
6110
|
+
state.recent[firstFreshIndex].atMs < cutoff) {
|
|
6111
|
+
firstFreshIndex += 1;
|
|
6112
|
+
}
|
|
6113
|
+
if (firstFreshIndex > 0) {
|
|
6114
|
+
state.recent.splice(0, firstFreshIndex);
|
|
6115
|
+
}
|
|
6116
|
+
}
|
|
6117
|
+
function recordOutcome(host, ok) {
|
|
6118
|
+
const now = Date.now();
|
|
6119
|
+
const state = getCircuitState(host);
|
|
6120
|
+
pruneStaleSamples(state, now);
|
|
6121
|
+
state.recent.push({ atMs: now, ok });
|
|
6122
|
+
if (state.recent.length > CIRCUIT_WINDOW_SIZE) {
|
|
6123
|
+
state.recent.shift();
|
|
6124
|
+
}
|
|
6125
|
+
// If we are in the open or half-open window and just received an
|
|
6126
|
+
// outcome, decide whether to close or re-open.
|
|
6127
|
+
if (state.openedAt > 0) {
|
|
6128
|
+
if (now - state.openedAt >= CIRCUIT_OPEN_COOLDOWN_MS) {
|
|
6129
|
+
// Half-open probe outcome arrived.
|
|
6130
|
+
if (ok) {
|
|
6131
|
+
// Recovered. Close the breaker.
|
|
6132
|
+
state.openedAt = 0;
|
|
6133
|
+
state.lastTripFailureRatio = 0;
|
|
6134
|
+
getLogger().info(`Circuit breaker for ${host} closed — upstream recovered`, { host });
|
|
6135
|
+
}
|
|
6136
|
+
else {
|
|
6137
|
+
// Probe failed. Re-open the breaker for another cooldown.
|
|
6138
|
+
state.openedAt = now;
|
|
6139
|
+
}
|
|
6140
|
+
return;
|
|
6141
|
+
}
|
|
6142
|
+
// Still inside the cooldown window — outcomes are recorded for
|
|
6143
|
+
// statistics but the breaker stays open regardless.
|
|
6144
|
+
return;
|
|
6145
|
+
}
|
|
6146
|
+
// Closed breaker — evaluate whether the failure ratio has tripped.
|
|
6147
|
+
if (state.recent.length < CIRCUIT_MIN_SAMPLES)
|
|
6148
|
+
return;
|
|
6149
|
+
const failures = state.recent.reduce((acc, entry) => acc + (entry.ok ? 0 : 1), 0);
|
|
6150
|
+
const ratio = failures / state.recent.length;
|
|
6151
|
+
if (ratio >= CIRCUIT_TRIP_FAILURE_RATIO) {
|
|
6152
|
+
state.openedAt = now;
|
|
6153
|
+
state.lastTripFailureRatio = ratio;
|
|
6154
|
+
getLogger().warn(`Circuit breaker for ${host} opened — ${failures}/${state.recent.length} recent attempts failed (${(ratio * 100).toFixed(0)}%); fail-fast for ${CIRCUIT_OPEN_COOLDOWN_MS}ms`, {
|
|
6155
|
+
host,
|
|
6156
|
+
recentFailures: failures,
|
|
6157
|
+
recentSamples: state.recent.length,
|
|
6158
|
+
failureRatio: ratio,
|
|
6159
|
+
cooldownMs: CIRCUIT_OPEN_COOLDOWN_MS,
|
|
6160
|
+
});
|
|
6161
|
+
}
|
|
6162
|
+
}
|
|
6163
|
+
function isCircuitOpen(host, now) {
|
|
6164
|
+
const state = circuitStates.get(host);
|
|
6165
|
+
if (!state || state.openedAt === 0)
|
|
6166
|
+
return false;
|
|
6167
|
+
pruneStaleSamples(state, now);
|
|
6168
|
+
return now - state.openedAt < CIRCUIT_OPEN_COOLDOWN_MS;
|
|
6169
|
+
}
|
|
6170
|
+
/**
|
|
6171
|
+
* Error thrown by {@link fetchWithRetry} when the per-host circuit
|
|
6172
|
+
* breaker is open and fail-fast suppression is in effect.
|
|
6173
|
+
*
|
|
6174
|
+
* Carries the host, the failure ratio that tripped the breaker, and
|
|
6175
|
+
* the remaining cooldown so callers can render an actionable log.
|
|
6176
|
+
*/
|
|
6177
|
+
class CircuitOpenError extends Error {
|
|
6178
|
+
code = "MASSIVE_CIRCUIT_OPEN";
|
|
6179
|
+
host;
|
|
6180
|
+
tripFailureRatio;
|
|
6181
|
+
cooldownRemainingMs;
|
|
6182
|
+
constructor(host, tripFailureRatio, cooldownRemainingMs) {
|
|
6183
|
+
super(`Circuit open for ${host} — fail-fast (recent failure ratio ${(tripFailureRatio * 100).toFixed(0)}%, retry in ${cooldownRemainingMs}ms)`);
|
|
6184
|
+
this.name = "CircuitOpenError";
|
|
6185
|
+
this.host = host;
|
|
6186
|
+
this.tripFailureRatio = tripFailureRatio;
|
|
6187
|
+
this.cooldownRemainingMs = cooldownRemainingMs;
|
|
6188
|
+
}
|
|
6189
|
+
}
|
|
6190
|
+
/**
|
|
6191
|
+
* Force-close the breaker for a given host. Exposed for tests and
|
|
6192
|
+
* operator-runbook scripts so a stuck-open breaker can be reset
|
|
6193
|
+
* without bouncing the process. Not intended for hot-path use.
|
|
6194
|
+
*
|
|
6195
|
+
* @param host The hostname whose breaker should be reset.
|
|
6196
|
+
*/
|
|
6197
|
+
function resetCircuitBreaker(host) {
|
|
6198
|
+
const state = circuitStates.get(host);
|
|
6199
|
+
if (!state)
|
|
6200
|
+
return;
|
|
6201
|
+
state.openedAt = 0;
|
|
6202
|
+
state.lastTripFailureRatio = 0;
|
|
6203
|
+
state.recent = [];
|
|
6204
|
+
}
|
|
6205
|
+
/**
|
|
6206
|
+
* Snapshot of all known per-host circuit-breaker states. Intended for
|
|
6207
|
+
* an operational-truth / status endpoint to surface upstream health.
|
|
6208
|
+
*
|
|
6209
|
+
* @returns Map of host → {open, openedAt, recentSamples, failureRatio,
|
|
6210
|
+
* lastTripFailureRatio}.
|
|
6211
|
+
*/
|
|
6212
|
+
function getCircuitBreakerSnapshot() {
|
|
6213
|
+
const now = Date.now();
|
|
6214
|
+
const snapshot = {};
|
|
6215
|
+
for (const [host, state] of circuitStates) {
|
|
6216
|
+
pruneStaleSamples(state, now);
|
|
6217
|
+
const failures = state.recent.reduce((acc, entry) => acc + (entry.ok ? 0 : 1), 0);
|
|
6218
|
+
const ratio = state.recent.length > 0 ? failures / state.recent.length : 0;
|
|
6219
|
+
snapshot[host] = {
|
|
6220
|
+
open: state.openedAt > 0 && now - state.openedAt < CIRCUIT_OPEN_COOLDOWN_MS,
|
|
6221
|
+
openedAt: state.openedAt,
|
|
6222
|
+
cooldownRemainingMs: state.openedAt > 0
|
|
6223
|
+
? Math.max(0, CIRCUIT_OPEN_COOLDOWN_MS - (now - state.openedAt))
|
|
6224
|
+
: 0,
|
|
6225
|
+
recentSamples: state.recent.length,
|
|
6226
|
+
failureRatio: ratio,
|
|
6227
|
+
lastTripFailureRatio: state.lastTripFailureRatio,
|
|
6228
|
+
};
|
|
6229
|
+
}
|
|
6230
|
+
return snapshot;
|
|
6231
|
+
}
|
|
6232
|
+
function hostnameFromUrl(url) {
|
|
6233
|
+
try {
|
|
6234
|
+
return new URL(url).hostname;
|
|
6235
|
+
}
|
|
6236
|
+
catch {
|
|
6237
|
+
return null;
|
|
6238
|
+
}
|
|
6239
|
+
}
|
|
6240
|
+
function sleep(ms) {
|
|
6241
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6242
|
+
}
|
|
6047
6243
|
/**
|
|
6048
6244
|
* Debug logging utility that respects environment debug flags.
|
|
6049
6245
|
* Logs messages through the configured structured logger when LUMIC_DEBUG
|
|
@@ -6155,9 +6351,46 @@ function hideApiKeyFromurl(url) {
|
|
|
6155
6351
|
* @throws Will throw an error if the fetch fails after the specified number of retries.
|
|
6156
6352
|
*/
|
|
6157
6353
|
async function fetchWithRetry(url, options = {}, retries = 3, initialBackoff = 1000) {
|
|
6354
|
+
// Per-host circuit-breaker check. When upstream is failing
|
|
6355
|
+
// pervasively (e.g. Massive REST in an outage), short-circuit to a
|
|
6356
|
+
// 100 ms fail-fast rejection so the caller's outer timeout (25 s,
|
|
6357
|
+
// 10 s, etc.) does not pile concurrent symbol fetches against a
|
|
6358
|
+
// dead host. Without this, an upstream outage during a screener tick
|
|
6359
|
+
// can lock up the event loop for up to (timeout × N-concurrent-symbols).
|
|
6360
|
+
const host = hostnameFromUrl(url);
|
|
6361
|
+
if (host) {
|
|
6362
|
+
const now = Date.now();
|
|
6363
|
+
if (isCircuitOpen(host, now)) {
|
|
6364
|
+
const state = getCircuitState(host);
|
|
6365
|
+
const cooldownRemainingMs = Math.max(0, CIRCUIT_OPEN_COOLDOWN_MS - (now - state.openedAt));
|
|
6366
|
+
await sleep(CIRCUIT_FAIL_FAST_LATENCY_MS);
|
|
6367
|
+
throw new CircuitOpenError(host, state.lastTripFailureRatio, cooldownRemainingMs);
|
|
6368
|
+
}
|
|
6369
|
+
}
|
|
6158
6370
|
return withRetry(async () => {
|
|
6159
|
-
|
|
6371
|
+
let response;
|
|
6372
|
+
try {
|
|
6373
|
+
response = await fetch(url, options);
|
|
6374
|
+
}
|
|
6375
|
+
catch (networkError) {
|
|
6376
|
+
// Network failure (e.g. DNS, connection refused, TLS abort).
|
|
6377
|
+
// Record against the breaker so a sustained outage trips it
|
|
6378
|
+
// for subsequent calls; then re-throw so withRetry handles
|
|
6379
|
+
// its own retry policy.
|
|
6380
|
+
if (host)
|
|
6381
|
+
recordOutcome(host, false);
|
|
6382
|
+
throw networkError;
|
|
6383
|
+
}
|
|
6160
6384
|
if (!response.ok) {
|
|
6385
|
+
// Classify the outcome for the circuit breaker. 5xx and 429
|
|
6386
|
+
// are upstream-health signals (record as failure). 4xx client
|
|
6387
|
+
// errors are caller-side and should NOT affect the breaker —
|
|
6388
|
+
// a request with a bad API key shouldn't trip the host's
|
|
6389
|
+
// breaker for everyone else.
|
|
6390
|
+
const upstreamUnhealthy = response.status === 429 ||
|
|
6391
|
+
(response.status >= 500 && response.status < 600);
|
|
6392
|
+
if (host)
|
|
6393
|
+
recordOutcome(host, !upstreamUnhealthy);
|
|
6161
6394
|
// Enhanced HTTP error handling with specific error types
|
|
6162
6395
|
if (response.status === 429) {
|
|
6163
6396
|
// Check for Retry-After header
|
|
@@ -6189,6 +6422,10 @@ async function fetchWithRetry(url, options = {}, retries = 3, initialBackoff = 1
|
|
|
6189
6422
|
error.response = response;
|
|
6190
6423
|
throw error;
|
|
6191
6424
|
}
|
|
6425
|
+
// Success — record against the breaker so a healthy upstream
|
|
6426
|
+
// closes any half-open state cleanly.
|
|
6427
|
+
if (host)
|
|
6428
|
+
recordOutcome(host, true);
|
|
6192
6429
|
return response;
|
|
6193
6430
|
}, {
|
|
6194
6431
|
maxRetries: retries,
|
|
@@ -7804,6 +8041,81 @@ const fetchTickerNews = async (ticker, options = {
|
|
|
7804
8041
|
});
|
|
7805
8042
|
};
|
|
7806
8043
|
|
|
8044
|
+
/**
|
|
8045
|
+
* Wilder ATR + EWMA-smoothed + multi-timespan variants.
|
|
8046
|
+
*
|
|
8047
|
+
* All functions are pure: same inputs → same output, no I/O, no time-dependence.
|
|
8048
|
+
*/
|
|
8049
|
+
/**
|
|
8050
|
+
* Classic Wilder ATR (single value).
|
|
8051
|
+
* @returns ATR for the most recent `period` bars, or null when insufficient bars.
|
|
8052
|
+
*/
|
|
8053
|
+
function calculateATR(highs, lows, closes, period) {
|
|
8054
|
+
if (period < 1 || !Number.isInteger(period)) {
|
|
8055
|
+
throw new Error("ATR: period must be a positive integer");
|
|
8056
|
+
}
|
|
8057
|
+
if (highs.length !== lows.length || highs.length !== closes.length) {
|
|
8058
|
+
throw new Error("ATR: highs, lows, closes must have equal length");
|
|
8059
|
+
}
|
|
8060
|
+
if (highs.length < period + 1)
|
|
8061
|
+
return null;
|
|
8062
|
+
const trs = [];
|
|
8063
|
+
for (let i = 1; i < highs.length; i++) {
|
|
8064
|
+
const tr = Math.max(highs[i] - lows[i], Math.abs(highs[i] - closes[i - 1]), Math.abs(lows[i] - closes[i - 1]));
|
|
8065
|
+
trs.push(tr);
|
|
8066
|
+
}
|
|
8067
|
+
// Wilder smoothing: first ATR = simple mean of first `period` TRs
|
|
8068
|
+
let atr = trs.slice(0, period).reduce((a, b) => a + b, 0) / period;
|
|
8069
|
+
for (let i = period; i < trs.length; i++) {
|
|
8070
|
+
atr = (atr * (period - 1) + trs[i]) / period;
|
|
8071
|
+
}
|
|
8072
|
+
return atr;
|
|
8073
|
+
}
|
|
8074
|
+
/**
|
|
8075
|
+
* Returns the EWMA-smoothed ATR series (one ATR per bar, leading nulls until `period` bars).
|
|
8076
|
+
*/
|
|
8077
|
+
function calculateATREMA(highs, lows, closes, period) {
|
|
8078
|
+
if (period < 1 || !Number.isInteger(period)) {
|
|
8079
|
+
throw new Error("ATR: period must be a positive integer");
|
|
8080
|
+
}
|
|
8081
|
+
if (highs.length !== lows.length || highs.length !== closes.length) {
|
|
8082
|
+
throw new Error("ATREMA: highs, lows, closes must have equal length");
|
|
8083
|
+
}
|
|
8084
|
+
const out = highs.map(() => null);
|
|
8085
|
+
if (highs.length < period + 1)
|
|
8086
|
+
return out;
|
|
8087
|
+
// Index 0 is a placeholder so trs[i] aligns with bar i (i >= 1); TR is undefined for the first bar (no prior close).
|
|
8088
|
+
const trs = [0];
|
|
8089
|
+
for (let i = 1; i < highs.length; i++) {
|
|
8090
|
+
trs.push(Math.max(highs[i] - lows[i], Math.abs(highs[i] - closes[i - 1]), Math.abs(lows[i] - closes[i - 1])));
|
|
8091
|
+
}
|
|
8092
|
+
let atr = trs.slice(1, period + 1).reduce((a, b) => a + b, 0) / period;
|
|
8093
|
+
out[period] = atr;
|
|
8094
|
+
for (let i = period + 1; i < trs.length; i++) {
|
|
8095
|
+
atr = (atr * (period - 1) + trs[i]) / period;
|
|
8096
|
+
out[i] = atr;
|
|
8097
|
+
}
|
|
8098
|
+
return out;
|
|
8099
|
+
}
|
|
8100
|
+
/**
|
|
8101
|
+
* Wrapper that accepts AtrBar[] (timespan-tagged); identical math, the timespan tag is
|
|
8102
|
+
* carried through for consumer-side logic only.
|
|
8103
|
+
*
|
|
8104
|
+
* @param bars - All bars must share the same timespan; the tag is metadata only
|
|
8105
|
+
* and is not validated. Mixed-timespan input produces a meaningless ATR.
|
|
8106
|
+
* @param period - Wilder lookback period (positive integer).
|
|
8107
|
+
*/
|
|
8108
|
+
function calculateATRMultiTimespan(bars, period) {
|
|
8109
|
+
return calculateATR(bars.map((b) => b.high), bars.map((b) => b.low), bars.map((b) => b.close), period);
|
|
8110
|
+
}
|
|
8111
|
+
|
|
8112
|
+
var atrNs = /*#__PURE__*/Object.freeze({
|
|
8113
|
+
__proto__: null,
|
|
8114
|
+
calculateATR: calculateATR,
|
|
8115
|
+
calculateATREMA: calculateATREMA,
|
|
8116
|
+
calculateATRMultiTimespan: calculateATRMultiTimespan
|
|
8117
|
+
});
|
|
8118
|
+
|
|
7807
8119
|
const ALPACA_API_BASE = MARKET_DATA_API.CRYPTO;
|
|
7808
8120
|
/**
|
|
7809
8121
|
* Fetches cryptocurrency bars for the specified parameters.
|
|
@@ -10853,6 +11165,335 @@ async function fetchPerformanceMetrics({ params, client, accountId, alpacaAccoun
|
|
|
10853
11165
|
}
|
|
10854
11166
|
}
|
|
10855
11167
|
|
|
11168
|
+
/**
|
|
11169
|
+
* VaR, Expected Shortfall (CVaR), conditional drawdown, rolling drawdown, Sortino, Calmar.
|
|
11170
|
+
*
|
|
11171
|
+
* Convention: VaR and ES are returned as the actual quantile value (typically negative
|
|
11172
|
+
* for losses). Drawdowns from `calculateConditionalDrawdown` are returned as non-negative
|
|
11173
|
+
* magnitudes (e.g., 0.05 = 5% drawdown). Drawdowns from `calculateRollingDrawdown` are
|
|
11174
|
+
* non-positive (e.g., -0.05 = 5% below rolling peak, 0 = at or above peak).
|
|
11175
|
+
*
|
|
11176
|
+
* All public functions reject non-finite inputs (NaN, Infinity) by throwing. Callers
|
|
11177
|
+
* must pre-validate or filter their inputs.
|
|
11178
|
+
*/
|
|
11179
|
+
function assertAlpha(alpha) {
|
|
11180
|
+
if (!(alpha > 0 && alpha < 1)) {
|
|
11181
|
+
throw new Error(`alpha must be in (0,1), got ${alpha}`);
|
|
11182
|
+
}
|
|
11183
|
+
}
|
|
11184
|
+
function assertFiniteArray$1(name, arr) {
|
|
11185
|
+
for (let i = 0; i < arr.length; i++) {
|
|
11186
|
+
if (!Number.isFinite(arr[i])) {
|
|
11187
|
+
throw new Error(`${name}: input contains non-finite value at index ${i}: ${arr[i]}`);
|
|
11188
|
+
}
|
|
11189
|
+
}
|
|
11190
|
+
}
|
|
11191
|
+
/**
|
|
11192
|
+
* Historical-bootstrap VaR at confidence `alpha`.
|
|
11193
|
+
* E.g., alpha=0.95 returns the 5%-quantile of returns (the loss at the 5th percentile).
|
|
11194
|
+
*
|
|
11195
|
+
* @returns The quantile value (typically negative), or null on empty input.
|
|
11196
|
+
*/
|
|
11197
|
+
function calculateVaRHistorical(returns, alpha) {
|
|
11198
|
+
assertAlpha(alpha);
|
|
11199
|
+
if (returns.length === 0)
|
|
11200
|
+
return null;
|
|
11201
|
+
assertFiniteArray$1("calculateVaRHistorical", returns);
|
|
11202
|
+
const sorted = [...returns].sort((a, b) => a - b);
|
|
11203
|
+
const idx = Math.max(0, Math.floor((1 - alpha) * sorted.length) - 1);
|
|
11204
|
+
return sorted[idx];
|
|
11205
|
+
}
|
|
11206
|
+
/**
|
|
11207
|
+
* Gaussian parametric VaR: μ + zα·σ where zα is the (1-alpha) standard-normal quantile.
|
|
11208
|
+
*
|
|
11209
|
+
* @returns The Gaussian quantile, or null when fewer than 2 samples.
|
|
11210
|
+
*/
|
|
11211
|
+
function calculateVaRParametric(returns, alpha) {
|
|
11212
|
+
assertAlpha(alpha);
|
|
11213
|
+
if (returns.length < 2)
|
|
11214
|
+
return null;
|
|
11215
|
+
assertFiniteArray$1("calculateVaRParametric", returns);
|
|
11216
|
+
const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
|
|
11217
|
+
const variance = returns.reduce((a, b) => a + (b - mean) ** 2, 0) / (returns.length - 1);
|
|
11218
|
+
const sigma = Math.sqrt(variance);
|
|
11219
|
+
const z = inverseStdNormal(1 - alpha);
|
|
11220
|
+
return mean + z * sigma;
|
|
11221
|
+
}
|
|
11222
|
+
/**
|
|
11223
|
+
* Expected Shortfall (Conditional VaR): average of returns below the (1-alpha) quantile.
|
|
11224
|
+
*
|
|
11225
|
+
* @returns The mean tail return (typically negative), or null on empty input.
|
|
11226
|
+
*/
|
|
11227
|
+
function calculateExpectedShortfall(returns, alpha) {
|
|
11228
|
+
assertAlpha(alpha);
|
|
11229
|
+
if (returns.length === 0)
|
|
11230
|
+
return null;
|
|
11231
|
+
assertFiniteArray$1("calculateExpectedShortfall", returns);
|
|
11232
|
+
const sorted = [...returns].sort((a, b) => a - b);
|
|
11233
|
+
const cutoff = Math.max(1, Math.floor((1 - alpha) * sorted.length));
|
|
11234
|
+
const tail = sorted.slice(0, cutoff);
|
|
11235
|
+
return tail.reduce((a, b) => a + b, 0) / tail.length;
|
|
11236
|
+
}
|
|
11237
|
+
/**
|
|
11238
|
+
* Conditional Drawdown at Risk (CDaR): average of drawdowns in the worst (1-alpha) tail.
|
|
11239
|
+
* Drawdowns are computed as (peak - equity) / peak so they are non-negative.
|
|
11240
|
+
*
|
|
11241
|
+
* @returns A non-negative magnitude (0 = no drawdowns), or null for fewer than 2 samples.
|
|
11242
|
+
*/
|
|
11243
|
+
function calculateConditionalDrawdown(equity, alpha) {
|
|
11244
|
+
assertAlpha(alpha);
|
|
11245
|
+
if (equity.length < 2)
|
|
11246
|
+
return null;
|
|
11247
|
+
assertFiniteArray$1("calculateConditionalDrawdown", equity);
|
|
11248
|
+
let peak = equity[0];
|
|
11249
|
+
const drawdowns = [];
|
|
11250
|
+
for (const e of equity) {
|
|
11251
|
+
if (e > peak)
|
|
11252
|
+
peak = e;
|
|
11253
|
+
drawdowns.push(peak > 0 ? (peak - e) / peak : 0);
|
|
11254
|
+
}
|
|
11255
|
+
if (drawdowns.every((d) => d === 0))
|
|
11256
|
+
return 0;
|
|
11257
|
+
const sorted = [...drawdowns].sort((a, b) => b - a); // descending (worst first)
|
|
11258
|
+
const cutoff = Math.max(1, Math.floor((1 - alpha) * sorted.length));
|
|
11259
|
+
const tail = sorted.slice(0, cutoff);
|
|
11260
|
+
return tail.reduce((a, b) => a + b, 0) / tail.length;
|
|
11261
|
+
}
|
|
11262
|
+
/**
|
|
11263
|
+
* Rolling-window drawdown series: for each index, drawdown = (current - rollingPeak) / rollingPeak.
|
|
11264
|
+
* Non-positive values; 0 when at or above the rolling peak. Window measured in samples.
|
|
11265
|
+
*
|
|
11266
|
+
* @returns An array the same length as `equity`.
|
|
11267
|
+
*/
|
|
11268
|
+
function calculateRollingDrawdown(equity, windowSize) {
|
|
11269
|
+
if (windowSize < 1 || !Number.isInteger(windowSize)) {
|
|
11270
|
+
throw new Error("calculateRollingDrawdown: windowSize must be a positive integer");
|
|
11271
|
+
}
|
|
11272
|
+
assertFiniteArray$1("calculateRollingDrawdown", equity);
|
|
11273
|
+
return equity.map((_, i) => {
|
|
11274
|
+
const start = Math.max(0, i - windowSize + 1);
|
|
11275
|
+
const slice = equity.slice(start, i + 1);
|
|
11276
|
+
const peak = Math.max(...slice);
|
|
11277
|
+
return peak > 0 ? (equity[i] - peak) / peak : 0;
|
|
11278
|
+
});
|
|
11279
|
+
}
|
|
11280
|
+
/**
|
|
11281
|
+
* Sortino ratio: (mean excess return) / downside deviation.
|
|
11282
|
+
* Returns +Infinity when there are no downside returns.
|
|
11283
|
+
* Returns null when fewer than 2 samples.
|
|
11284
|
+
*/
|
|
11285
|
+
function calculateSortino(returns, riskFreeRate) {
|
|
11286
|
+
if (returns.length < 2)
|
|
11287
|
+
return null;
|
|
11288
|
+
assertFiniteArray$1("calculateSortino", returns);
|
|
11289
|
+
const excess = returns.map((r) => r - riskFreeRate);
|
|
11290
|
+
const meanExcess = excess.reduce((a, b) => a + b, 0) / excess.length;
|
|
11291
|
+
const downside = excess.filter((r) => r < 0);
|
|
11292
|
+
if (downside.length === 0)
|
|
11293
|
+
return Number.POSITIVE_INFINITY;
|
|
11294
|
+
const dd = Math.sqrt(downside.reduce((a, b) => a + b * b, 0) / downside.length);
|
|
11295
|
+
return meanExcess / dd;
|
|
11296
|
+
}
|
|
11297
|
+
/**
|
|
11298
|
+
* Calmar ratio: CAGR / |max drawdown|.
|
|
11299
|
+
*
|
|
11300
|
+
* @returns null when there is no drawdown (division by zero), fewer than 2 samples,
|
|
11301
|
+
* or `equity[0] <= 0` (CAGR undefined).
|
|
11302
|
+
*/
|
|
11303
|
+
function calculateCalmar(equity, periodsPerYear) {
|
|
11304
|
+
if (equity.length < 2)
|
|
11305
|
+
return null;
|
|
11306
|
+
if (equity[0] <= 0)
|
|
11307
|
+
return null;
|
|
11308
|
+
if (periodsPerYear <= 0) {
|
|
11309
|
+
throw new Error("calculateCalmar: periodsPerYear must be > 0");
|
|
11310
|
+
}
|
|
11311
|
+
assertFiniteArray$1("calculateCalmar", equity);
|
|
11312
|
+
const total = equity[equity.length - 1] / equity[0];
|
|
11313
|
+
const years = (equity.length - 1) / periodsPerYear;
|
|
11314
|
+
const cagr = years > 0 ? Math.pow(total, 1 / years) - 1 : 0;
|
|
11315
|
+
let peak = equity[0];
|
|
11316
|
+
let maxDd = 0;
|
|
11317
|
+
for (const e of equity) {
|
|
11318
|
+
if (e > peak)
|
|
11319
|
+
peak = e;
|
|
11320
|
+
const dd = peak > 0 ? (peak - e) / peak : 0;
|
|
11321
|
+
if (dd > maxDd)
|
|
11322
|
+
maxDd = dd;
|
|
11323
|
+
}
|
|
11324
|
+
return maxDd === 0 ? null : cagr / maxDd;
|
|
11325
|
+
}
|
|
11326
|
+
/**
|
|
11327
|
+
* Beasley-Springer-Moro approximation of the inverse standard normal CDF.
|
|
11328
|
+
* Accurate to ~1e-9 across the full domain; sufficient for VaR work.
|
|
11329
|
+
*/
|
|
11330
|
+
function inverseStdNormal(p) {
|
|
11331
|
+
if (p <= 0 || p >= 1)
|
|
11332
|
+
throw new Error("p must be in (0,1)");
|
|
11333
|
+
const a = [-39.69683028665376, 2.209460984245205e2, -275.9285104469687,
|
|
11334
|
+
1.38357751867269e2, -30.66479806614716, 2.506628277459239];
|
|
11335
|
+
const b = [-54.47609879822406, 1.615858368580409e2, -155.6989798598866,
|
|
11336
|
+
6.680131188771972e1, -13.28068155288572];
|
|
11337
|
+
const c = [-0.007784894002430293, -0.3223964580411365, -2.400758277161838,
|
|
11338
|
+
-2.549732539343734, 4.374664141464968, 2.938163982698783];
|
|
11339
|
+
const d = [7.784695709041462e-3, 3.224671290700398e-1, 2.445134137142996,
|
|
11340
|
+
3.754408661907416];
|
|
11341
|
+
const pLow = 0.02425;
|
|
11342
|
+
const pHigh = 1 - pLow;
|
|
11343
|
+
let q, r;
|
|
11344
|
+
if (p < pLow) {
|
|
11345
|
+
q = Math.sqrt(-2 * Math.log(p));
|
|
11346
|
+
return (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) /
|
|
11347
|
+
((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1);
|
|
11348
|
+
}
|
|
11349
|
+
if (p <= pHigh) {
|
|
11350
|
+
q = p - 0.5;
|
|
11351
|
+
r = q * q;
|
|
11352
|
+
return (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q /
|
|
11353
|
+
(((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1);
|
|
11354
|
+
}
|
|
11355
|
+
q = Math.sqrt(-2 * Math.log(1 - p));
|
|
11356
|
+
return -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) /
|
|
11357
|
+
((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1);
|
|
11358
|
+
}
|
|
11359
|
+
|
|
11360
|
+
var riskNs = /*#__PURE__*/Object.freeze({
|
|
11361
|
+
__proto__: null,
|
|
11362
|
+
calculateCalmar: calculateCalmar,
|
|
11363
|
+
calculateConditionalDrawdown: calculateConditionalDrawdown,
|
|
11364
|
+
calculateExpectedShortfall: calculateExpectedShortfall,
|
|
11365
|
+
calculateRollingDrawdown: calculateRollingDrawdown,
|
|
11366
|
+
calculateSortino: calculateSortino,
|
|
11367
|
+
calculateVaRHistorical: calculateVaRHistorical,
|
|
11368
|
+
calculateVaRParametric: calculateVaRParametric
|
|
11369
|
+
});
|
|
11370
|
+
|
|
11371
|
+
/**
|
|
11372
|
+
* Per-strategy rolling metrics and backtest-divergence z-score.
|
|
11373
|
+
*
|
|
11374
|
+
* Conventions:
|
|
11375
|
+
* - tradePnls / tradeReturns is an array of per-trade realised P&L or return
|
|
11376
|
+
* (positive = win, negative = loss, zero = breakeven).
|
|
11377
|
+
* - All "rolling*" functions return null when fewer than `windowSize` trades exist.
|
|
11378
|
+
* - All public functions reject non-finite inputs (NaN, Infinity) by throwing.
|
|
11379
|
+
* Callers must pre-validate or filter their inputs.
|
|
11380
|
+
*/
|
|
11381
|
+
function assertWindowSize(name, windowSize) {
|
|
11382
|
+
if (windowSize < 1 || !Number.isInteger(windowSize)) {
|
|
11383
|
+
throw new Error(`${name}: windowSize must be a positive integer`);
|
|
11384
|
+
}
|
|
11385
|
+
}
|
|
11386
|
+
function assertFiniteArray(name, arr) {
|
|
11387
|
+
for (let i = 0; i < arr.length; i++) {
|
|
11388
|
+
if (!Number.isFinite(arr[i])) {
|
|
11389
|
+
throw new Error(`${name}: input contains non-finite value at index ${i}: ${arr[i]}`);
|
|
11390
|
+
}
|
|
11391
|
+
}
|
|
11392
|
+
}
|
|
11393
|
+
/**
|
|
11394
|
+
* Rolling expectancy: mean P&L over the most-recent `windowSize` trades.
|
|
11395
|
+
*
|
|
11396
|
+
* @param tradePnls - Array of per-trade realised P&L values.
|
|
11397
|
+
* @param windowSize - Number of most-recent trades to include. Must be a positive integer.
|
|
11398
|
+
* @returns Mean P&L of the last `windowSize` trades, or null when fewer than `windowSize` exist.
|
|
11399
|
+
* @throws When `windowSize` is not a positive integer or any input is non-finite.
|
|
11400
|
+
*/
|
|
11401
|
+
function calculateRollingExpectancy(tradePnls, windowSize) {
|
|
11402
|
+
assertWindowSize("calculateRollingExpectancy", windowSize);
|
|
11403
|
+
if (tradePnls.length < windowSize)
|
|
11404
|
+
return null;
|
|
11405
|
+
assertFiniteArray("calculateRollingExpectancy", tradePnls);
|
|
11406
|
+
const slice = tradePnls.slice(-windowSize);
|
|
11407
|
+
return slice.reduce((a, b) => a + b, 0) / windowSize;
|
|
11408
|
+
}
|
|
11409
|
+
/**
|
|
11410
|
+
* Rolling hit-rate: fraction of strictly-positive P&L trades in the most-recent
|
|
11411
|
+
* `windowSize` trades. Zero P&L counts as non-win.
|
|
11412
|
+
*
|
|
11413
|
+
* @param tradePnls - Array of per-trade realised P&L values.
|
|
11414
|
+
* @param windowSize - Number of most-recent trades to include. Must be a positive integer.
|
|
11415
|
+
* @returns Fraction of winning trades in the window, or null when fewer than `windowSize` exist.
|
|
11416
|
+
* @throws When `windowSize` is not a positive integer or any input is non-finite.
|
|
11417
|
+
*/
|
|
11418
|
+
function calculateRollingHitRate(tradePnls, windowSize) {
|
|
11419
|
+
assertWindowSize("calculateRollingHitRate", windowSize);
|
|
11420
|
+
if (tradePnls.length < windowSize)
|
|
11421
|
+
return null;
|
|
11422
|
+
assertFiniteArray("calculateRollingHitRate", tradePnls);
|
|
11423
|
+
const slice = tradePnls.slice(-windowSize);
|
|
11424
|
+
const wins = slice.filter((p) => p > 0).length;
|
|
11425
|
+
return wins / windowSize;
|
|
11426
|
+
}
|
|
11427
|
+
/**
|
|
11428
|
+
* Rolling profit factor: sum(wins) / |sum(losses)| over the most-recent `windowSize` trades.
|
|
11429
|
+
*
|
|
11430
|
+
* Edge cases:
|
|
11431
|
+
* - no losses and at least one win → +Infinity
|
|
11432
|
+
* - no wins and no losses (all zeros) → 0
|
|
11433
|
+
* - fewer than windowSize trades → null
|
|
11434
|
+
*
|
|
11435
|
+
* @param tradePnls - Array of per-trade realised P&L values.
|
|
11436
|
+
* @param windowSize - Number of most-recent trades to include. Must be a positive integer.
|
|
11437
|
+
* @returns Profit factor for the rolling window, or null when fewer than `windowSize` exist.
|
|
11438
|
+
* @throws When `windowSize` is not a positive integer or any input is non-finite.
|
|
11439
|
+
*/
|
|
11440
|
+
function calculateRollingProfitFactor(tradePnls, windowSize) {
|
|
11441
|
+
assertWindowSize("calculateRollingProfitFactor", windowSize);
|
|
11442
|
+
if (tradePnls.length < windowSize)
|
|
11443
|
+
return null;
|
|
11444
|
+
assertFiniteArray("calculateRollingProfitFactor", tradePnls);
|
|
11445
|
+
const slice = tradePnls.slice(-windowSize);
|
|
11446
|
+
const wins = slice.filter((p) => p > 0).reduce((a, b) => a + b, 0);
|
|
11447
|
+
const losses = slice.filter((p) => p < 0).reduce((a, b) => a + Math.abs(b), 0);
|
|
11448
|
+
if (losses === 0)
|
|
11449
|
+
return wins > 0 ? Number.POSITIVE_INFINITY : 0;
|
|
11450
|
+
return wins / losses;
|
|
11451
|
+
}
|
|
11452
|
+
/**
|
|
11453
|
+
* Rolling Sortino: delegate to `calculateSortino` over the most-recent `windowSize` returns.
|
|
11454
|
+
*
|
|
11455
|
+
* @param tradeReturns - Array of per-trade return values.
|
|
11456
|
+
* @param windowSize - Number of most-recent trades to include. Must be a positive integer.
|
|
11457
|
+
* @param riskFreeRate - Risk-free rate to subtract from returns (default 0).
|
|
11458
|
+
* @returns Sortino ratio for the rolling window, or null when fewer than `windowSize` exist.
|
|
11459
|
+
* @throws When `windowSize` is not a positive integer or any input is non-finite.
|
|
11460
|
+
*/
|
|
11461
|
+
function calculateRollingSortino(tradeReturns, windowSize, riskFreeRate = 0) {
|
|
11462
|
+
assertWindowSize("calculateRollingSortino", windowSize);
|
|
11463
|
+
if (tradeReturns.length < windowSize)
|
|
11464
|
+
return null;
|
|
11465
|
+
assertFiniteArray("calculateRollingSortino", tradeReturns);
|
|
11466
|
+
return calculateSortino(tradeReturns.slice(-windowSize), riskFreeRate);
|
|
11467
|
+
}
|
|
11468
|
+
/**
|
|
11469
|
+
* Z-score of live-expectancy vs backtest-expectancy, scaled by the backtest stddev.
|
|
11470
|
+
* Positive Z = live outperforming; negative Z = live underperforming.
|
|
11471
|
+
*
|
|
11472
|
+
* @param liveExpectancy - Mean P&L per trade in the live window.
|
|
11473
|
+
* @param backtestExpectancy - Mean P&L per trade from the calibration backtest.
|
|
11474
|
+
* @param backtestStddev - Stddev of per-trade P&L in the backtest. Must be > 0.
|
|
11475
|
+
* @returns Z-score measuring divergence between live and backtest performance.
|
|
11476
|
+
* @throws When any input is non-finite or `backtestStddev` is not positive.
|
|
11477
|
+
*/
|
|
11478
|
+
function calculateBacktestDivergenceZ(liveExpectancy, backtestExpectancy, backtestStddev) {
|
|
11479
|
+
if (!Number.isFinite(liveExpectancy) || !Number.isFinite(backtestExpectancy) || !Number.isFinite(backtestStddev)) {
|
|
11480
|
+
throw new Error("calculateBacktestDivergenceZ: inputs must be finite numbers");
|
|
11481
|
+
}
|
|
11482
|
+
if (backtestStddev <= 0) {
|
|
11483
|
+
throw new Error("calculateBacktestDivergenceZ: stddev must be > 0");
|
|
11484
|
+
}
|
|
11485
|
+
return (liveExpectancy - backtestExpectancy) / backtestStddev;
|
|
11486
|
+
}
|
|
11487
|
+
|
|
11488
|
+
var strategyNs = /*#__PURE__*/Object.freeze({
|
|
11489
|
+
__proto__: null,
|
|
11490
|
+
calculateBacktestDivergenceZ: calculateBacktestDivergenceZ,
|
|
11491
|
+
calculateRollingExpectancy: calculateRollingExpectancy,
|
|
11492
|
+
calculateRollingHitRate: calculateRollingHitRate,
|
|
11493
|
+
calculateRollingProfitFactor: calculateRollingProfitFactor,
|
|
11494
|
+
calculateRollingSortino: calculateRollingSortino
|
|
11495
|
+
});
|
|
11496
|
+
|
|
10856
11497
|
/**
|
|
10857
11498
|
* Calculates Bollinger Bands for a given set of price data.
|
|
10858
11499
|
* Bollinger Bands consist of a middle band (SMA) and two outer bands
|
|
@@ -11479,6 +12120,98 @@ var Types = /*#__PURE__*/Object.freeze({
|
|
|
11479
12120
|
__proto__: null
|
|
11480
12121
|
});
|
|
11481
12122
|
|
|
12123
|
+
/**
|
|
12124
|
+
* Realized and EWMA volatility + regime classifier + annualisation helper.
|
|
12125
|
+
* All functions pure.
|
|
12126
|
+
*/
|
|
12127
|
+
/**
|
|
12128
|
+
* Sample standard deviation (Bessel-corrected) of returns over the most recent
|
|
12129
|
+
* `window` samples.
|
|
12130
|
+
* @returns null when fewer than `window` samples.
|
|
12131
|
+
*/
|
|
12132
|
+
function calculateRealizedVolatility(returns, window) {
|
|
12133
|
+
if (window < 2 || !Number.isInteger(window)) {
|
|
12134
|
+
throw new Error("calculateRealizedVolatility: window must be an integer >= 2");
|
|
12135
|
+
}
|
|
12136
|
+
if (returns.length < window)
|
|
12137
|
+
return null;
|
|
12138
|
+
const slice = returns.slice(-window);
|
|
12139
|
+
const mean = slice.reduce((a, b) => a + b, 0) / window;
|
|
12140
|
+
const variance = slice.reduce((a, b) => a + (b - mean) ** 2, 0) / (window - 1);
|
|
12141
|
+
return Math.sqrt(variance);
|
|
12142
|
+
}
|
|
12143
|
+
/**
|
|
12144
|
+
* EWMA volatility (RiskMetrics-style). λ ∈ (0,1); higher = longer memory.
|
|
12145
|
+
* Default usage: λ = 0.94 for daily returns.
|
|
12146
|
+
*
|
|
12147
|
+
* For a single-element input, the function returns `|returns[0]|` (the seed)
|
|
12148
|
+
* since no smoothing iterations are possible.
|
|
12149
|
+
*
|
|
12150
|
+
* @param returns - Period returns (e.g., log returns or simple returns).
|
|
12151
|
+
* @param lambda - Decay factor in (0,1).
|
|
12152
|
+
* @returns EWMA standard deviation, or null on empty input.
|
|
12153
|
+
* @throws when `lambda` is outside (0,1).
|
|
12154
|
+
*/
|
|
12155
|
+
function calculateEWMAVolatility(returns, lambda) {
|
|
12156
|
+
if (lambda <= 0 || lambda >= 1) {
|
|
12157
|
+
throw new Error("calculateEWMAVolatility: lambda must be in (0,1)");
|
|
12158
|
+
}
|
|
12159
|
+
if (returns.length === 0)
|
|
12160
|
+
return null;
|
|
12161
|
+
let variance = returns[0] ** 2;
|
|
12162
|
+
for (let i = 1; i < returns.length; i++) {
|
|
12163
|
+
variance = lambda * variance + (1 - lambda) * returns[i] ** 2;
|
|
12164
|
+
}
|
|
12165
|
+
return Math.sqrt(variance);
|
|
12166
|
+
}
|
|
12167
|
+
/**
|
|
12168
|
+
* Classify a volatility value into one of four regimes.
|
|
12169
|
+
*
|
|
12170
|
+
* Bands are checked in the order: crisis (≥crisisMin) → elevated (≥elevatedMax)
|
|
12171
|
+
* → calm (≤calmMax) → normal (otherwise).
|
|
12172
|
+
*
|
|
12173
|
+
* @throws when bands are not strictly ordered (calmMax < elevatedMax < crisisMin).
|
|
12174
|
+
*/
|
|
12175
|
+
function detectVolatilityRegime(volatility, bands) {
|
|
12176
|
+
if (!(bands.calmMax < bands.elevatedMax && bands.elevatedMax < bands.crisisMin)) {
|
|
12177
|
+
throw new Error(`detectVolatilityRegime: bands must satisfy calmMax < elevatedMax < crisisMin (got ${JSON.stringify(bands)})`);
|
|
12178
|
+
}
|
|
12179
|
+
if (volatility >= bands.crisisMin)
|
|
12180
|
+
return "crisis";
|
|
12181
|
+
if (volatility >= bands.elevatedMax)
|
|
12182
|
+
return "elevated";
|
|
12183
|
+
if (volatility <= bands.calmMax)
|
|
12184
|
+
return "calm";
|
|
12185
|
+
return "normal";
|
|
12186
|
+
}
|
|
12187
|
+
/**
|
|
12188
|
+
* Annualise a volatility computed at the given cadence by multiplying by
|
|
12189
|
+
* the square root of the periods per year.
|
|
12190
|
+
*
|
|
12191
|
+
* - daily → sqrt(252) (252 trading days per year)
|
|
12192
|
+
* - hourly → sqrt(252 × 6.5) (6.5 RTH hours per trading day)
|
|
12193
|
+
* - minute → sqrt(252 × 6.5 × 60) (60 minutes per RTH hour)
|
|
12194
|
+
*
|
|
12195
|
+
* @param volatility - Per-period volatility (stddev).
|
|
12196
|
+
* @param cadence - The cadence at which `volatility` was sampled.
|
|
12197
|
+
* @returns The annualised volatility.
|
|
12198
|
+
*/
|
|
12199
|
+
function annualiseVolatility(volatility, cadence) {
|
|
12200
|
+
switch (cadence) {
|
|
12201
|
+
case "daily": return volatility * Math.sqrt(252);
|
|
12202
|
+
case "hourly": return volatility * Math.sqrt(252 * 6.5);
|
|
12203
|
+
case "minute": return volatility * Math.sqrt(252 * 6.5 * 60);
|
|
12204
|
+
}
|
|
12205
|
+
}
|
|
12206
|
+
|
|
12207
|
+
var volatilityNs = /*#__PURE__*/Object.freeze({
|
|
12208
|
+
__proto__: null,
|
|
12209
|
+
annualiseVolatility: annualiseVolatility,
|
|
12210
|
+
calculateEWMAVolatility: calculateEWMAVolatility,
|
|
12211
|
+
calculateRealizedVolatility: calculateRealizedVolatility,
|
|
12212
|
+
detectVolatilityRegime: detectVolatilityRegime
|
|
12213
|
+
});
|
|
12214
|
+
|
|
11482
12215
|
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
11483
12216
|
|
|
11484
12217
|
function getDefaultExportFromCjs (x) {
|
|
@@ -68697,6 +69430,10 @@ const createAlpacaMarketDataAPI = () => {
|
|
|
68697
69430
|
};
|
|
68698
69431
|
const adaptic = {
|
|
68699
69432
|
types: Types,
|
|
69433
|
+
atr: atrNs,
|
|
69434
|
+
risk: riskNs,
|
|
69435
|
+
strategy: strategyNs,
|
|
69436
|
+
volatility: volatilityNs,
|
|
68700
69437
|
backend: {
|
|
68701
69438
|
fetchAssetOverview: fetchAssetOverview,
|
|
68702
69439
|
getApolloClient: getSharedApolloClient,
|
|
@@ -68879,6 +69616,17 @@ const adaptic = {
|
|
|
68879
69616
|
logIfDebug: logIfDebug,
|
|
68880
69617
|
fetchWithRetry: fetchWithRetry,
|
|
68881
69618
|
validateMassiveApiKey: validateMassiveApiKey,
|
|
69619
|
+
/**
|
|
69620
|
+
* Force-close a stuck-open per-host circuit breaker. Operator
|
|
69621
|
+
* runbook utility — see {@link misc.resetCircuitBreaker}.
|
|
69622
|
+
*/
|
|
69623
|
+
resetCircuitBreaker: resetCircuitBreaker,
|
|
69624
|
+
/**
|
|
69625
|
+
* Read-only snapshot of all per-host circuit-breaker states for
|
|
69626
|
+
* use in operational-truth endpoints. See
|
|
69627
|
+
* {@link misc.getCircuitBreakerSnapshot}.
|
|
69628
|
+
*/
|
|
69629
|
+
getCircuitBreakerSnapshot: getCircuitBreakerSnapshot,
|
|
68882
69630
|
},
|
|
68883
69631
|
rateLimiter: {
|
|
68884
69632
|
TokenBucketRateLimiter,
|
|
@@ -68917,6 +69665,7 @@ exports.AssetAllocationEngine = AssetAllocationEngine;
|
|
|
68917
69665
|
exports.AuthenticationError = AuthenticationError;
|
|
68918
69666
|
exports.BTC_PAIRS = BTC_PAIRS;
|
|
68919
69667
|
exports.BarError = BarError;
|
|
69668
|
+
exports.CircuitOpenError = CircuitOpenError;
|
|
68920
69669
|
exports.CryptoDataError = CryptoDataError;
|
|
68921
69670
|
exports.CryptoOrderError = CryptoOrderError;
|
|
68922
69671
|
exports.DEFAULT_CACHE_OPTIONS = DEFAULT_CACHE_OPTIONS;
|
|
@@ -68965,6 +69714,7 @@ exports.adptc = adptc;
|
|
|
68965
69714
|
exports.alpaca = alpaca;
|
|
68966
69715
|
exports.analyzeBars = analyzeBars;
|
|
68967
69716
|
exports.approximateImpliedVolatility = approximateImpliedVolatility;
|
|
69717
|
+
exports.atr = atrNs;
|
|
68968
69718
|
exports.bracketOrders = bracketOrders;
|
|
68969
69719
|
exports.buildOCCSymbol = buildOCCSymbol;
|
|
68970
69720
|
exports.buildOptionSymbol = buildOptionSymbol;
|
|
@@ -69147,6 +69897,7 @@ exports.protectShortPosition = protectShortPosition;
|
|
|
69147
69897
|
exports.rateLimiters = rateLimiters;
|
|
69148
69898
|
exports.resetLogger = resetLogger;
|
|
69149
69899
|
exports.resetRiskFreeRateCache = resetRiskFreeRateCache;
|
|
69900
|
+
exports.risk = riskNs;
|
|
69150
69901
|
exports.rollOptionPosition = rollOptionPosition;
|
|
69151
69902
|
exports.roundPriceForAlpaca = roundPriceForAlpaca$3;
|
|
69152
69903
|
exports.roundPriceForAlpacaNumber = roundPriceForAlpacaNumber;
|
|
@@ -69160,6 +69911,7 @@ exports.setLogger = setLogger;
|
|
|
69160
69911
|
exports.setRiskFreeRate = setRiskFreeRate;
|
|
69161
69912
|
exports.shortWithStopLoss = shortWithStopLoss;
|
|
69162
69913
|
exports.sortOrdersByDate = sortOrdersByDate;
|
|
69914
|
+
exports.strategy = strategyNs;
|
|
69163
69915
|
exports.tradingPolicy = index;
|
|
69164
69916
|
exports.trailingStops = trailingStops;
|
|
69165
69917
|
exports.updateAccountConfiguration = updateAccountConfiguration;
|
|
@@ -69170,6 +69922,7 @@ exports.validateMassiveApiKey = validateMassiveApiKey$1;
|
|
|
69170
69922
|
exports.validateMultiLegOrder = validateMultiLegOrder;
|
|
69171
69923
|
exports.validateResponse = validateResponse;
|
|
69172
69924
|
exports.verifyFetchKeepAlive = verifyFetchKeepAlive;
|
|
69925
|
+
exports.volatility = volatilityNs;
|
|
69173
69926
|
exports.waitForOrderFill = waitForOrderFill;
|
|
69174
69927
|
exports.withRetry = withRetry;
|
|
69175
69928
|
exports.withTimeout = withTimeout;
|