orderflow-metrics 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +229 -0
- package/LICENSE +21 -0
- package/README.md +556 -0
- package/dist/bars.d.ts +67 -0
- package/dist/bars.d.ts.map +1 -0
- package/dist/bars.js +92 -0
- package/dist/bars.js.map +1 -0
- package/dist/classify.d.ts +25 -0
- package/dist/classify.d.ts.map +1 -0
- package/dist/classify.js +57 -0
- package/dist/classify.js.map +1 -0
- package/dist/covariance.d.ts +37 -0
- package/dist/covariance.d.ts.map +1 -0
- package/dist/covariance.js +72 -0
- package/dist/covariance.js.map +1 -0
- package/dist/efficiency.d.ts +24 -0
- package/dist/efficiency.d.ts.map +1 -0
- package/dist/efficiency.js +65 -0
- package/dist/efficiency.js.map +1 -0
- package/dist/entropy.d.ts +40 -0
- package/dist/entropy.d.ts.map +1 -0
- package/dist/entropy.js +73 -0
- package/dist/entropy.js.map +1 -0
- package/dist/execution.d.ts +40 -0
- package/dist/execution.d.ts.map +1 -0
- package/dist/execution.js +64 -0
- package/dist/execution.js.map +1 -0
- package/dist/fairvalue.d.ts +19 -0
- package/dist/fairvalue.d.ts.map +1 -0
- package/dist/fairvalue.js +27 -0
- package/dist/fairvalue.js.map +1 -0
- package/dist/hurst.d.ts +23 -0
- package/dist/hurst.d.ts.map +1 -0
- package/dist/hurst.js +71 -0
- package/dist/hurst.js.map +1 -0
- package/dist/imbalance.d.ts +12 -0
- package/dist/imbalance.d.ts.map +1 -0
- package/dist/imbalance.js +19 -0
- package/dist/imbalance.js.map +1 -0
- package/dist/impact.d.ts +59 -0
- package/dist/impact.d.ts.map +1 -0
- package/dist/impact.js +63 -0
- package/dist/impact.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/jumps.d.ts +33 -0
- package/dist/jumps.d.ts.map +1 -0
- package/dist/jumps.js +63 -0
- package/dist/jumps.js.map +1 -0
- package/dist/liquidity.d.ts +18 -0
- package/dist/liquidity.d.ts.map +1 -0
- package/dist/liquidity.js +22 -0
- package/dist/liquidity.js.map +1 -0
- package/dist/moments.d.ts +20 -0
- package/dist/moments.d.ts.map +1 -0
- package/dist/moments.js +48 -0
- package/dist/moments.js.map +1 -0
- package/dist/ofi.d.ts +22 -0
- package/dist/ofi.d.ts.map +1 -0
- package/dist/ofi.js +25 -0
- package/dist/ofi.js.map +1 -0
- package/dist/online.d.ts +101 -0
- package/dist/online.d.ts.map +1 -0
- package/dist/online.js +205 -0
- package/dist/online.js.map +1 -0
- package/dist/orderbook.d.ts +30 -0
- package/dist/orderbook.d.ts.map +1 -0
- package/dist/orderbook.js +66 -0
- package/dist/orderbook.js.map +1 -0
- package/dist/rangevol.d.ts +40 -0
- package/dist/rangevol.d.ts.map +1 -0
- package/dist/rangevol.js +101 -0
- package/dist/rangevol.js.map +1 -0
- package/dist/scheduling.d.ts +19 -0
- package/dist/scheduling.d.ts.map +1 -0
- package/dist/scheduling.js +45 -0
- package/dist/scheduling.js.map +1 -0
- package/dist/semivar.d.ts +49 -0
- package/dist/semivar.d.ts.map +1 -0
- package/dist/semivar.js +59 -0
- package/dist/semivar.js.map +1 -0
- package/dist/shortfall.d.ts +42 -0
- package/dist/shortfall.d.ts.map +1 -0
- package/dist/shortfall.js +32 -0
- package/dist/shortfall.js.map +1 -0
- package/dist/simulate.d.ts +29 -0
- package/dist/simulate.d.ts.map +1 -0
- package/dist/simulate.js +36 -0
- package/dist/simulate.js.map +1 -0
- package/dist/spreadest.d.ts +38 -0
- package/dist/spreadest.d.ts.map +1 -0
- package/dist/spreadest.js +68 -0
- package/dist/spreadest.js.map +1 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/volatility.d.ts +21 -0
- package/dist/volatility.d.ts.map +1 -0
- package/dist/volatility.js +33 -0
- package/dist/volatility.js.map +1 -0
- package/dist/vpin.d.ts +38 -0
- package/dist/vpin.d.ts.map +1 -0
- package/dist/vpin.js +81 -0
- package/dist/vpin.js.map +1 -0
- package/package.json +70 -0
- package/src/bars.ts +141 -0
- package/src/classify.ts +59 -0
- package/src/covariance.ts +72 -0
- package/src/efficiency.ts +64 -0
- package/src/entropy.ts +67 -0
- package/src/execution.ts +100 -0
- package/src/fairvalue.ts +33 -0
- package/src/hurst.ts +77 -0
- package/src/imbalance.ts +25 -0
- package/src/impact.ts +99 -0
- package/src/index.ts +87 -0
- package/src/jumps.ts +61 -0
- package/src/liquidity.ts +29 -0
- package/src/moments.ts +45 -0
- package/src/ofi.ts +46 -0
- package/src/online.ts +228 -0
- package/src/orderbook.ts +76 -0
- package/src/rangevol.ts +108 -0
- package/src/scheduling.ts +50 -0
- package/src/semivar.ts +67 -0
- package/src/shortfall.ts +69 -0
- package/src/simulate.ts +68 -0
- package/src/spreadest.ts +75 -0
- package/src/types.ts +26 -0
- package/src/volatility.ts +36 -0
- package/src/vpin.ts +98 -0
package/src/online.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Online (streaming) estimators — O(1) per update, no rescans.
|
|
3
|
+
*
|
|
4
|
+
* Batch metrics recompute over the whole history every time a tick arrives;
|
|
5
|
+
* that is fine offline but wasteful in a live pipeline. The estimators here
|
|
6
|
+
* update in constant time and constant memory as each observation streams in,
|
|
7
|
+
* which is what you want feeding volatility or risk off a real-time tape.
|
|
8
|
+
*
|
|
9
|
+
* They are numerically careful: `Welford` and `RollingWindow` use Welford's
|
|
10
|
+
* and West's algorithms rather than the naive Σx² − (Σx)²/n form, which loses
|
|
11
|
+
* precision catastrophically when the mean is large relative to the variance.
|
|
12
|
+
*
|
|
13
|
+
* Welford — running mean & variance over all data seen so far
|
|
14
|
+
* Ewma — exponentially weighted moving average (a level)
|
|
15
|
+
* EwmaVariance — RiskMetrics-style EWMA variance (a volatility)
|
|
16
|
+
* RollingWindow — mean & variance over a fixed trailing window
|
|
17
|
+
*
|
|
18
|
+
* Sample variance uses the (n − 1) denominator; each class also exposes the
|
|
19
|
+
* population (n) form. Variance getters are floored at 0 to absorb the tiny
|
|
20
|
+
* negative values floating-point round-off can produce near zero.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Streaming mean and variance over every value pushed so far, via Welford's
|
|
25
|
+
* online algorithm. Constant time and memory per update, and numerically
|
|
26
|
+
* stable regardless of the mean's magnitude.
|
|
27
|
+
*/
|
|
28
|
+
export class Welford {
|
|
29
|
+
#n = 0;
|
|
30
|
+
#mean = 0;
|
|
31
|
+
#m2 = 0;
|
|
32
|
+
|
|
33
|
+
/** Incorporate one observation. */
|
|
34
|
+
push(x: number): void {
|
|
35
|
+
this.#n += 1;
|
|
36
|
+
const delta = x - this.#mean;
|
|
37
|
+
this.#mean += delta / this.#n;
|
|
38
|
+
this.#m2 += delta * (x - this.#mean);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Number of observations seen. */
|
|
42
|
+
get count(): number {
|
|
43
|
+
return this.#n;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Running mean (0 before any observation). */
|
|
47
|
+
get mean(): number {
|
|
48
|
+
return this.#n > 0 ? this.#mean : 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Sample variance, (n − 1) denominator (0 for fewer than two observations). */
|
|
52
|
+
get variance(): number {
|
|
53
|
+
if (this.#n < 2) return 0;
|
|
54
|
+
const v = this.#m2 / (this.#n - 1);
|
|
55
|
+
return v > 0 ? v : 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Population variance, n denominator (0 before any observation). */
|
|
59
|
+
get populationVariance(): number {
|
|
60
|
+
if (this.#n < 1) return 0;
|
|
61
|
+
const v = this.#m2 / this.#n;
|
|
62
|
+
return v > 0 ? v : 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Sample standard deviation. */
|
|
66
|
+
get std(): number {
|
|
67
|
+
return Math.sqrt(this.variance);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Exponentially weighted moving average of a level: vₜ = λ·vₜ₋₁ + (1 − λ)·xₜ.
|
|
73
|
+
* `lambda` is the weight on history (decay), in (0, 1) — larger is smoother and
|
|
74
|
+
* slower to react. Seeded with the first value pushed.
|
|
75
|
+
*/
|
|
76
|
+
export class Ewma {
|
|
77
|
+
readonly #lambda: number;
|
|
78
|
+
#value = 0;
|
|
79
|
+
#init = false;
|
|
80
|
+
|
|
81
|
+
constructor(lambda: number) {
|
|
82
|
+
if (!(lambda > 0 && lambda < 1)) {
|
|
83
|
+
throw new RangeError("lambda must be in the open interval (0, 1)");
|
|
84
|
+
}
|
|
85
|
+
this.#lambda = lambda;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Incorporate one observation. */
|
|
89
|
+
push(x: number): void {
|
|
90
|
+
this.#value = this.#init ? this.#lambda * this.#value + (1 - this.#lambda) * x : x;
|
|
91
|
+
this.#init = true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Current EWMA level (0 before any observation). */
|
|
95
|
+
get value(): number {
|
|
96
|
+
return this.#value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Whether at least one value has been pushed. */
|
|
100
|
+
get initialized(): boolean {
|
|
101
|
+
return this.#init;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* RiskMetrics-style exponentially weighted variance of a return series:
|
|
107
|
+
* σ²ₜ = λ·σ²ₜ₋₁ + (1 − λ)·r²ₜ. Assumes approximately zero-mean returns (the
|
|
108
|
+
* standard RiskMetrics assumption). `lambda` in (0, 1) is the decay; RiskMetrics
|
|
109
|
+
* uses 0.94 for daily data. Seeded with r² of the first value pushed.
|
|
110
|
+
*/
|
|
111
|
+
export class EwmaVariance {
|
|
112
|
+
readonly #lambda: number;
|
|
113
|
+
#var = 0;
|
|
114
|
+
#init = false;
|
|
115
|
+
|
|
116
|
+
constructor(lambda: number) {
|
|
117
|
+
if (!(lambda > 0 && lambda < 1)) {
|
|
118
|
+
throw new RangeError("lambda must be in the open interval (0, 1)");
|
|
119
|
+
}
|
|
120
|
+
this.#lambda = lambda;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Incorporate one return. */
|
|
124
|
+
push(r: number): void {
|
|
125
|
+
this.#var = this.#init ? this.#lambda * this.#var + (1 - this.#lambda) * r * r : r * r;
|
|
126
|
+
this.#init = true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Current EWMA variance (0 before any observation). */
|
|
130
|
+
get variance(): number {
|
|
131
|
+
return this.#var;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Current EWMA volatility (standard deviation). */
|
|
135
|
+
get std(): number {
|
|
136
|
+
return Math.sqrt(this.#var);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Whether at least one value has been pushed. */
|
|
140
|
+
get initialized(): boolean {
|
|
141
|
+
return this.#init;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Mean and variance over a fixed trailing window of the last `size` values.
|
|
147
|
+
* Each push is O(1): the incoming value is added and, once the window is full,
|
|
148
|
+
* the oldest is removed, both via West's (1979) incremental update — so there is
|
|
149
|
+
* no per-tick rescan and no Σx² cancellation.
|
|
150
|
+
*/
|
|
151
|
+
export class RollingWindow {
|
|
152
|
+
readonly #size: number;
|
|
153
|
+
readonly #buf: number[] = [];
|
|
154
|
+
#idx = 0;
|
|
155
|
+
#n = 0;
|
|
156
|
+
#mean = 0;
|
|
157
|
+
#m2 = 0;
|
|
158
|
+
|
|
159
|
+
constructor(size: number) {
|
|
160
|
+
if (!Number.isInteger(size) || size < 1) {
|
|
161
|
+
throw new RangeError("size must be a positive integer");
|
|
162
|
+
}
|
|
163
|
+
this.#size = size;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Push one value, evicting the oldest once the window is full. */
|
|
167
|
+
push(x: number): void {
|
|
168
|
+
if (this.#n < this.#size) {
|
|
169
|
+
// Window not yet full: plain Welford add.
|
|
170
|
+
this.#n += 1;
|
|
171
|
+
const delta = x - this.#mean;
|
|
172
|
+
this.#mean += delta / this.#n;
|
|
173
|
+
this.#m2 += delta * (x - this.#mean);
|
|
174
|
+
this.#buf.push(x);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Full: add the newcomer, then remove the oldest (West's add + remove).
|
|
178
|
+
const old = this.#buf[this.#idx];
|
|
179
|
+
const n1 = this.#n + 1;
|
|
180
|
+
const delta = x - this.#mean;
|
|
181
|
+
const mean1 = this.#mean + delta / n1;
|
|
182
|
+
const m2Added = this.#m2 + delta * (x - mean1);
|
|
183
|
+
const mean0 = (n1 * mean1 - old) / this.#size;
|
|
184
|
+
this.#m2 = m2Added - (old - mean0) * (old - mean1);
|
|
185
|
+
this.#mean = mean0;
|
|
186
|
+
this.#buf[this.#idx] = x;
|
|
187
|
+
this.#idx = (this.#idx + 1) % this.#size;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Configured window size. */
|
|
191
|
+
get size(): number {
|
|
192
|
+
return this.#size;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Number of values currently in the window (≤ size). */
|
|
196
|
+
get count(): number {
|
|
197
|
+
return this.#n;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Whether the window has filled to `size`. */
|
|
201
|
+
get full(): boolean {
|
|
202
|
+
return this.#n === this.#size;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Mean of the current window (0 when empty). */
|
|
206
|
+
get mean(): number {
|
|
207
|
+
return this.#n > 0 ? this.#mean : 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Sample variance of the current window (0 for fewer than two values). */
|
|
211
|
+
get variance(): number {
|
|
212
|
+
if (this.#n < 2) return 0;
|
|
213
|
+
const v = this.#m2 / (this.#n - 1);
|
|
214
|
+
return v > 0 ? v : 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Population variance of the current window (0 when empty). */
|
|
218
|
+
get populationVariance(): number {
|
|
219
|
+
if (this.#n < 1) return 0;
|
|
220
|
+
const v = this.#m2 / this.#n;
|
|
221
|
+
return v > 0 ? v : 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Sample standard deviation of the current window. */
|
|
225
|
+
get std(): number {
|
|
226
|
+
return Math.sqrt(this.variance);
|
|
227
|
+
}
|
|
228
|
+
}
|
package/src/orderbook.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Limit order book reconstruction from incremental level updates.
|
|
3
|
+
*
|
|
4
|
+
* Feed it level updates (price + new size per side); it maintains both sides
|
|
5
|
+
* and answers the usual top-of-book and depth questions. A size of 0 removes
|
|
6
|
+
* the level. Prices are the level keys, so re-sending a price overwrites it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type BookSide = "bid" | "ask";
|
|
10
|
+
|
|
11
|
+
export interface Level {
|
|
12
|
+
price: number;
|
|
13
|
+
size: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class OrderBook {
|
|
17
|
+
private readonly bids = new Map<number, number>();
|
|
18
|
+
private readonly asks = new Map<number, number>();
|
|
19
|
+
|
|
20
|
+
/** Apply a level update. ``size <= 0`` removes the price level. */
|
|
21
|
+
update(side: BookSide, price: number, size: number): void {
|
|
22
|
+
const book = side === "bid" ? this.bids : this.asks;
|
|
23
|
+
if (size <= 0) book.delete(price);
|
|
24
|
+
else book.set(price, size);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
bestBid(): Level | null {
|
|
28
|
+
let best: Level | null = null;
|
|
29
|
+
for (const [price, size] of this.bids) {
|
|
30
|
+
if (best === null || price > best.price) best = { price, size };
|
|
31
|
+
}
|
|
32
|
+
return best;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
bestAsk(): Level | null {
|
|
36
|
+
let best: Level | null = null;
|
|
37
|
+
for (const [price, size] of this.asks) {
|
|
38
|
+
if (best === null || price < best.price) best = { price, size };
|
|
39
|
+
}
|
|
40
|
+
return best;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
mid(): number | null {
|
|
44
|
+
const b = this.bestBid();
|
|
45
|
+
const a = this.bestAsk();
|
|
46
|
+
return b && a ? (b.price + a.price) / 2 : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
spread(): number | null {
|
|
50
|
+
const b = this.bestBid();
|
|
51
|
+
const a = this.bestAsk();
|
|
52
|
+
return b && a ? a.price - b.price : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Top ``n`` levels of a side, best price first. */
|
|
56
|
+
depth(side: BookSide, n: number): Level[] {
|
|
57
|
+
const book = side === "bid" ? this.bids : this.asks;
|
|
58
|
+
const levels: Level[] = [...book.entries()].map(([price, size]) => ({
|
|
59
|
+
price,
|
|
60
|
+
size,
|
|
61
|
+
}));
|
|
62
|
+
levels.sort((x, y) => (side === "bid" ? y.price - x.price : x.price - y.price));
|
|
63
|
+
return levels.slice(0, n);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Book imbalance over the top ``n`` levels:
|
|
68
|
+
* (bidVolume − askVolume) / (bidVolume + askVolume), in [-1, 1].
|
|
69
|
+
*/
|
|
70
|
+
imbalance(n = 1): number {
|
|
71
|
+
const bidVol = this.depth("bid", n).reduce((s, l) => s + l.size, 0);
|
|
72
|
+
const askVol = this.depth("ask", n).reduce((s, l) => s + l.size, 0);
|
|
73
|
+
const denom = bidVol + askVol;
|
|
74
|
+
return denom === 0 ? 0 : (bidVol - askVol) / denom;
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/rangevol.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Range-based volatility estimators from OHLC candles.
|
|
3
|
+
*
|
|
4
|
+
* Close-to-close realized volatility (see `volatility`) throws away most of each
|
|
5
|
+
* bar — it reads only the close. But the open, high, and low carry information
|
|
6
|
+
* too, and using them yields far more efficient volatility estimates from the
|
|
7
|
+
* same data. Four classic estimators, in increasing order of what they use:
|
|
8
|
+
*
|
|
9
|
+
* - Parkinson (1980): the high-low range only.
|
|
10
|
+
* - Garman-Klass (1980): adds the open and close.
|
|
11
|
+
* - Rogers-Satchell (1991): drift-independent (correct under a trending mean).
|
|
12
|
+
* - Yang-Zhang (2000): drift-independent and robust to overnight jumps.
|
|
13
|
+
*
|
|
14
|
+
* Each returns the estimated volatility (standard deviation) *per bar*. To
|
|
15
|
+
* annualize, multiply the variance by the number of bars per year, or the
|
|
16
|
+
* volatility by its square root. Prices must be strictly positive.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface Candle {
|
|
20
|
+
open: number;
|
|
21
|
+
high: number;
|
|
22
|
+
low: number;
|
|
23
|
+
close: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const LN2 = Math.LN2;
|
|
27
|
+
|
|
28
|
+
/** Parkinson (1980) high-low volatility. Returns 0 for an empty input. */
|
|
29
|
+
export function parkinsonVolatility(bars: Candle[]): number {
|
|
30
|
+
const n = bars.length;
|
|
31
|
+
if (n === 0) return 0;
|
|
32
|
+
let s = 0;
|
|
33
|
+
for (const b of bars) {
|
|
34
|
+
const hl = Math.log(b.high / b.low);
|
|
35
|
+
s += hl * hl;
|
|
36
|
+
}
|
|
37
|
+
return Math.sqrt(s / (4 * LN2) / n);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Garman-Klass (1980) OHLC volatility. Returns 0 for an empty input. */
|
|
41
|
+
export function garmanKlassVolatility(bars: Candle[]): number {
|
|
42
|
+
const n = bars.length;
|
|
43
|
+
if (n === 0) return 0;
|
|
44
|
+
let s = 0;
|
|
45
|
+
for (const b of bars) {
|
|
46
|
+
const hl = Math.log(b.high / b.low);
|
|
47
|
+
const co = Math.log(b.close / b.open);
|
|
48
|
+
s += 0.5 * hl * hl - (2 * LN2 - 1) * co * co;
|
|
49
|
+
}
|
|
50
|
+
const v = s / n;
|
|
51
|
+
return v > 0 ? Math.sqrt(v) : 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Rogers-Satchell (1991) drift-independent OHLC volatility. Correct even when
|
|
56
|
+
* the price has a non-zero mean drift. Returns 0 for an empty input.
|
|
57
|
+
*/
|
|
58
|
+
export function rogersSatchellVolatility(bars: Candle[]): number {
|
|
59
|
+
const n = bars.length;
|
|
60
|
+
if (n === 0) return 0;
|
|
61
|
+
let s = 0;
|
|
62
|
+
for (const b of bars) {
|
|
63
|
+
s +=
|
|
64
|
+
Math.log(b.high / b.close) * Math.log(b.high / b.open) +
|
|
65
|
+
Math.log(b.low / b.close) * Math.log(b.low / b.open);
|
|
66
|
+
}
|
|
67
|
+
const v = s / n;
|
|
68
|
+
return v > 0 ? Math.sqrt(v) : 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Yang-Zhang (2000) volatility: a minimum-variance combination of overnight,
|
|
73
|
+
* open-to-close, and Rogers-Satchell variances that is both drift-independent
|
|
74
|
+
* and robust to opening jumps. Uses each bar's open relative to the previous
|
|
75
|
+
* close, so it needs at least three bars; returns 0 otherwise.
|
|
76
|
+
*/
|
|
77
|
+
export function yangZhangVolatility(bars: Candle[]): number {
|
|
78
|
+
const n = bars.length;
|
|
79
|
+
if (n < 3) return 0;
|
|
80
|
+
const overnight: number[] = [];
|
|
81
|
+
const openClose: number[] = [];
|
|
82
|
+
let rs = 0;
|
|
83
|
+
for (let i = 1; i < n; i++) {
|
|
84
|
+
const prev = bars[i - 1];
|
|
85
|
+
const b = bars[i];
|
|
86
|
+
overnight.push(Math.log(b.open / prev.close));
|
|
87
|
+
openClose.push(Math.log(b.close / b.open));
|
|
88
|
+
rs +=
|
|
89
|
+
Math.log(b.high / b.close) * Math.log(b.high / b.open) +
|
|
90
|
+
Math.log(b.low / b.close) * Math.log(b.low / b.open);
|
|
91
|
+
}
|
|
92
|
+
const m = overnight.length;
|
|
93
|
+
const mean = (a: number[]) => a.reduce((x, y) => x + y, 0) / a.length;
|
|
94
|
+
const ob = mean(overnight);
|
|
95
|
+
const cb = mean(openClose);
|
|
96
|
+
let so = 0;
|
|
97
|
+
let sc = 0;
|
|
98
|
+
for (let i = 0; i < m; i++) {
|
|
99
|
+
so += (overnight[i] - ob) ** 2;
|
|
100
|
+
sc += (openClose[i] - cb) ** 2;
|
|
101
|
+
}
|
|
102
|
+
so /= m - 1;
|
|
103
|
+
sc /= m - 1;
|
|
104
|
+
const rsv = rs / m;
|
|
105
|
+
const k = 0.34 / (1.34 + (m + 1) / (m - 1));
|
|
106
|
+
const v = so + k * sc + (1 - k) * rsv;
|
|
107
|
+
return v > 0 ? Math.sqrt(v) : 0;
|
|
108
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution scheduling: split a parent order into child slices.
|
|
3
|
+
*
|
|
4
|
+
* `twap` spreads size evenly across a fixed number of slices (time-weighted).
|
|
5
|
+
* `pov` participates at a fixed fraction of each interval's volume
|
|
6
|
+
* (percentage-of-volume). Both return the child sizes; sizes may be fractional.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* TWAP: split ``totalSize`` into ``slices`` equal child orders. The sizes sum
|
|
11
|
+
* exactly to ``totalSize`` (any floating residual lands in the last slice).
|
|
12
|
+
*/
|
|
13
|
+
export function twap(totalSize: number, slices: number): number[] {
|
|
14
|
+
if (slices < 1 || !Number.isInteger(slices)) {
|
|
15
|
+
throw new Error("slices must be a positive integer");
|
|
16
|
+
}
|
|
17
|
+
const out: number[] = [];
|
|
18
|
+
let allocated = 0;
|
|
19
|
+
for (let i = 1; i <= slices; i++) {
|
|
20
|
+
const target = (totalSize * i) / slices;
|
|
21
|
+
out.push(target - allocated);
|
|
22
|
+
allocated = target;
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* POV: for each interval, trade ``rate`` × that interval's volume, capped by
|
|
29
|
+
* the size still remaining. Returns per-interval child sizes; their sum is the
|
|
30
|
+
* filled amount (less than ``totalSize`` if the volume was insufficient).
|
|
31
|
+
*/
|
|
32
|
+
export function pov(
|
|
33
|
+
totalSize: number,
|
|
34
|
+
intervalVolumes: readonly number[],
|
|
35
|
+
rate: number,
|
|
36
|
+
): number[] {
|
|
37
|
+
const r = Math.max(0, rate);
|
|
38
|
+
const out: number[] = [];
|
|
39
|
+
let remaining = totalSize;
|
|
40
|
+
for (const vol of intervalVolumes) {
|
|
41
|
+
if (remaining <= 0) {
|
|
42
|
+
out.push(0);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const child = Math.min(remaining, r * Math.max(0, vol));
|
|
46
|
+
out.push(child);
|
|
47
|
+
remaining -= child;
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
package/src/semivar.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realized semivariance and signed jump variation
|
|
3
|
+
* (Barndorff-Nielsen, Kinnebrock & Shephard, 2010; Patton & Shephard, 2015).
|
|
4
|
+
*
|
|
5
|
+
* Realized variance (RV = Σ rᵢ²) treats an up-move and a down-move of equal
|
|
6
|
+
* size as identical risk. But most traders care far more about the downside.
|
|
7
|
+
* Realized semivariance splits RV by the *sign* of each return:
|
|
8
|
+
*
|
|
9
|
+
* RS⁺ = Σ rᵢ² · 1{rᵢ > 0} (upside)
|
|
10
|
+
* RS⁻ = Σ rᵢ² · 1{rᵢ < 0} (downside)
|
|
11
|
+
* RS⁺ + RS⁻ = RV (zero returns contribute to neither)
|
|
12
|
+
*
|
|
13
|
+
* The two halves carry different information: RS⁻ (bad volatility) is the part
|
|
14
|
+
* that predicts future risk and demands a premium, while RS⁺ (good volatility)
|
|
15
|
+
* behaves quite differently. Their difference is the *signed* jump variation,
|
|
16
|
+
* which — unlike ordinary jump variation — keeps the direction of jump risk:
|
|
17
|
+
*
|
|
18
|
+
* signed jump variation = RS⁺ − RS⁻
|
|
19
|
+
*
|
|
20
|
+
* A positive value means upside moves dominate; a negative value means the
|
|
21
|
+
* series is downside-heavy. Each function takes a return series.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Upside / downside decomposition of realized variance. Both are ≥ 0. */
|
|
25
|
+
export interface Semivariance {
|
|
26
|
+
/** RS⁺ = Σ rᵢ² over strictly positive returns. */
|
|
27
|
+
upside: number;
|
|
28
|
+
/** RS⁻ = Σ rᵢ² over strictly negative returns. */
|
|
29
|
+
downside: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Realized semivariance: splits realized variance into the sum of squared
|
|
34
|
+
* positive returns (upside) and squared negative returns (downside). Zero
|
|
35
|
+
* returns are ignored, so `upside + downside` equals realized variance. An
|
|
36
|
+
* empty series returns { upside: 0, downside: 0 }.
|
|
37
|
+
*/
|
|
38
|
+
export function realizedSemivariance(returns: readonly number[]): Semivariance {
|
|
39
|
+
let upside = 0;
|
|
40
|
+
let downside = 0;
|
|
41
|
+
for (const r of returns) {
|
|
42
|
+
if (r > 0) upside += r * r;
|
|
43
|
+
else if (r < 0) downside += r * r;
|
|
44
|
+
}
|
|
45
|
+
return { upside, downside };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Downside variance ratio: RS⁻ / (RS⁺ + RS⁻), the share of realized variance
|
|
50
|
+
* coming from negative returns, in [0, 1]. Returns 0 when realized variance is
|
|
51
|
+
* 0 (or the series is empty). A value above 0.5 marks a downside-heavy window.
|
|
52
|
+
*/
|
|
53
|
+
export function downsideVarianceRatio(returns: readonly number[]): number {
|
|
54
|
+
const { upside, downside } = realizedSemivariance(returns);
|
|
55
|
+
const total = upside + downside;
|
|
56
|
+
return total > 0 ? downside / total : 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Signed jump variation: RS⁺ − RS⁻ (Patton & Shephard, 2015). Positive when
|
|
61
|
+
* upside moves dominate, negative when downside moves dominate; unlike jump
|
|
62
|
+
* variation it can take either sign. Returns 0 for an empty series.
|
|
63
|
+
*/
|
|
64
|
+
export function signedJumpVariation(returns: readonly number[]): number {
|
|
65
|
+
const { upside, downside } = realizedSemivariance(returns);
|
|
66
|
+
return upside - downside;
|
|
67
|
+
}
|
package/src/shortfall.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Implementation shortfall and arrival slippage.
|
|
3
|
+
*
|
|
4
|
+
* Perold's (1988) "implementation shortfall" measures the all-in cost of turning
|
|
5
|
+
* a paper decision into real fills. It splits into the cost paid on the shares
|
|
6
|
+
* you actually executed (vs the price when you decided), the opportunity cost of
|
|
7
|
+
* the shares you failed to execute as the price drifted away, and fees. Arrival
|
|
8
|
+
* slippage is the simpler, per-order version against the arrival mid.
|
|
9
|
+
*
|
|
10
|
+
* Sign convention: buys are +1, sells are -1. All costs are returned as
|
|
11
|
+
* *positive = worse* (a shortfall), in the input price/quantity units.
|
|
12
|
+
*/
|
|
13
|
+
import type { Side } from "./types.ts";
|
|
14
|
+
|
|
15
|
+
function dir(side: Side): 1 | -1 {
|
|
16
|
+
return side === "buy" ? 1 : -1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ShortfallResult {
|
|
20
|
+
/** Cost on executed shares vs the decision price. */
|
|
21
|
+
execution: number;
|
|
22
|
+
/** Opportunity cost of unexecuted shares as the price drifted. */
|
|
23
|
+
opportunity: number;
|
|
24
|
+
/** Explicit fees / commissions. */
|
|
25
|
+
fees: number;
|
|
26
|
+
/** execution + opportunity + fees. Positive = shortfall (cost). */
|
|
27
|
+
total: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Implementation shortfall of an order, decomposed into execution cost,
|
|
32
|
+
* opportunity cost, and fees.
|
|
33
|
+
*
|
|
34
|
+
* @param decisionPrice price when the trade was decided (the paper benchmark)
|
|
35
|
+
* @param avgExecPrice volume-weighted average price actually paid/received
|
|
36
|
+
* @param executedQty shares actually filled
|
|
37
|
+
* @param targetQty shares originally intended
|
|
38
|
+
* @param finalPrice price at the end of trading (for the unfilled remainder)
|
|
39
|
+
* @param fees explicit costs (default 0)
|
|
40
|
+
*/
|
|
41
|
+
export function implementationShortfall(
|
|
42
|
+
side: Side,
|
|
43
|
+
decisionPrice: number,
|
|
44
|
+
avgExecPrice: number,
|
|
45
|
+
executedQty: number,
|
|
46
|
+
targetQty: number,
|
|
47
|
+
finalPrice: number,
|
|
48
|
+
fees = 0,
|
|
49
|
+
): ShortfallResult {
|
|
50
|
+
const d = dir(side);
|
|
51
|
+
const execution = d * (avgExecPrice - decisionPrice) * executedQty;
|
|
52
|
+
const unexecuted = Math.max(0, targetQty - executedQty);
|
|
53
|
+
const opportunity = d * (finalPrice - decisionPrice) * unexecuted;
|
|
54
|
+
return { execution, opportunity, fees, total: execution + opportunity + fees };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Arrival slippage in basis points: signed cost of the average execution price
|
|
59
|
+
* against the arrival price. Positive = adverse (paid up on a buy / sold low on
|
|
60
|
+
* a sell). Returns 0 when the arrival price is 0.
|
|
61
|
+
*/
|
|
62
|
+
export function arrivalSlippageBps(
|
|
63
|
+
side: Side,
|
|
64
|
+
arrivalPrice: number,
|
|
65
|
+
avgExecPrice: number,
|
|
66
|
+
): number {
|
|
67
|
+
if (arrivalPrice === 0) return 0;
|
|
68
|
+
return ((dir(side) * (avgExecPrice - arrivalPrice)) / arrivalPrice) * 10_000;
|
|
69
|
+
}
|
package/src/simulate.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Market-order simulation against a reconstructed order book.
|
|
3
|
+
*
|
|
4
|
+
* Sweep the book with a market order and see what you'd actually get: the
|
|
5
|
+
* volume-weighted fill price, slippage vs the starting mid, and any size the
|
|
6
|
+
* book was too thin to fill. Read-only — the book is not mutated.
|
|
7
|
+
*/
|
|
8
|
+
import { OrderBook } from "./orderbook.ts";
|
|
9
|
+
|
|
10
|
+
export interface Fill {
|
|
11
|
+
price: number;
|
|
12
|
+
size: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface MarketOrderResult {
|
|
16
|
+
filledSize: number;
|
|
17
|
+
remainingSize: number;
|
|
18
|
+
/** volume-weighted average fill price, or null if nothing filled */
|
|
19
|
+
avgPrice: number | null;
|
|
20
|
+
notional: number;
|
|
21
|
+
/** cost vs the mid at start, in basis points (positive = adverse); null if unknown */
|
|
22
|
+
slippageBps: number | null;
|
|
23
|
+
fills: Fill[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Simulate a market order. A ``buy`` consumes asks from best (lowest) upward;
|
|
28
|
+
* a ``sell`` consumes bids from best (highest) downward. Stops when filled or
|
|
29
|
+
* the book runs out (``remainingSize`` > 0).
|
|
30
|
+
*/
|
|
31
|
+
export function simulateMarketOrder(
|
|
32
|
+
book: OrderBook,
|
|
33
|
+
side: "buy" | "sell",
|
|
34
|
+
size: number,
|
|
35
|
+
): MarketOrderResult {
|
|
36
|
+
const startMid = book.mid();
|
|
37
|
+
const levels = book.depth(side === "buy" ? "ask" : "bid", Number.MAX_SAFE_INTEGER);
|
|
38
|
+
|
|
39
|
+
const fills: Fill[] = [];
|
|
40
|
+
let remaining = size;
|
|
41
|
+
let notional = 0;
|
|
42
|
+
|
|
43
|
+
for (const level of levels) {
|
|
44
|
+
if (remaining <= 0) break;
|
|
45
|
+
const take = Math.min(remaining, level.size);
|
|
46
|
+
fills.push({ price: level.price, size: take });
|
|
47
|
+
notional += level.price * take;
|
|
48
|
+
remaining -= take;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const filled = size - remaining;
|
|
52
|
+
const avgPrice = filled > 0 ? notional / filled : null;
|
|
53
|
+
|
|
54
|
+
let slippageBps: number | null = null;
|
|
55
|
+
if (avgPrice !== null && startMid !== null && startMid !== 0) {
|
|
56
|
+
const raw = side === "buy" ? avgPrice - startMid : startMid - avgPrice;
|
|
57
|
+
slippageBps = (raw / startMid) * 10_000;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
filledSize: filled,
|
|
62
|
+
remainingSize: remaining,
|
|
63
|
+
avgPrice,
|
|
64
|
+
notional,
|
|
65
|
+
slippageBps,
|
|
66
|
+
fills,
|
|
67
|
+
};
|
|
68
|
+
}
|