gloomberb 0.11.2 → 0.11.3
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/README.md +1 -0
- package/package.json +1 -1
- package/src/api-client/data.ts +5 -0
- package/src/api-client/index.ts +5 -0
- package/src/api-client/types.ts +24 -0
- package/src/capabilities/chart-series.ts +3 -2
- package/src/cli/commands/market.ts +2 -2
- package/src/cli/pane-functions/capabilities.ts +3 -3
- package/src/cli/registry.ts +0 -22
- package/src/cli/result.ts +7 -10
- package/src/components/data-table/export.test.ts +21 -0
- package/src/components/data-table/export.ts +21 -0
- package/src/components/ui/data-table/index.tsx +19 -2
- package/src/data/config/chart-settings.ts +4 -4
- package/src/layout-marketplace/gallery-terminal.tsx +126 -96
- package/src/layout-marketplace/gallery.test.tsx +65 -1
- package/src/plugins/builtin/chart-composer/cli-options.ts +12 -9
- package/src/plugins/builtin/chart-composer/settings.ts +2 -13
- package/src/plugins/builtin/composite-plugins.ts +2 -0
- package/src/plugins/builtin/earnings/index.tsx +1 -0
- package/src/plugins/builtin/econ/index.tsx +1 -0
- package/src/plugins/builtin/futures/index.tsx +1 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +1 -0
- package/src/plugins/builtin/ipo-calendar/index.tsx +1 -0
- package/src/plugins/builtin/market-halts/index.tsx +1 -0
- package/src/plugins/builtin/market-movers/index.tsx +1 -0
- package/src/plugins/builtin/options/analytics.test.ts +83 -0
- package/src/plugins/builtin/options/analytics.ts +110 -0
- package/src/plugins/builtin/options/index.tsx +17 -2
- package/src/plugins/builtin/options/table.ts +121 -51
- package/src/plugins/builtin/options/types.ts +22 -15
- package/src/plugins/builtin/options/view.test.tsx +24 -0
- package/src/plugins/builtin/options/view.tsx +91 -19
- package/src/plugins/builtin/plugin-module.test.ts +2 -4
- package/src/plugins/builtin/plugin-module.ts +0 -7
- package/src/plugins/builtin/portfolio-list/index.tsx +1 -0
- package/src/plugins/builtin/scanner/index.ts +2 -0
- package/src/plugins/builtin/sectors/index.tsx +1 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +3 -5
- package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +1 -0
- package/src/plugins/builtin/world-indices/index.tsx +1 -0
- package/src/plugins/builtin/world-venue-map/index.ts +27 -0
- package/src/plugins/builtin/world-venue-map/map.tsx +337 -0
- package/src/plugins/builtin/world-venue-map/model.test.ts +53 -0
- package/src/plugins/builtin/world-venue-map/model.ts +147 -0
- package/src/plugins/builtin/world-venue-map/pane.tsx +300 -0
- package/src/plugins/builtin/world-venue-map/world-outlines.ts +2 -0
- package/src/plugins/catalog-backend.ts +6 -44
- package/src/plugins/catalog-browser.ts +2 -0
- package/src/plugins/registry/pane-settings.test.ts +53 -0
- package/src/plugins/registry/pane-settings.ts +36 -3
- package/src/renderers/browser/ui-host.tsx +11 -0
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +9 -2
- package/src/renderers/electrobun/bun/index.ts +1 -0
- package/src/renderers/electrobun/shared/protocol.ts +4 -0
- package/src/renderers/electrobun/view/ui-host.tsx +3 -0
- package/src/renderers/opentui/host.tsx +4 -0
- package/src/state/pane-table-export-registry.ts +31 -0
- package/src/time-series/range.ts +12 -0
- package/src/time-series/resolution.ts +3 -15
- package/src/time-series/spec.ts +5 -5
- package/src/types/plugin.ts +2 -20
- package/src/ui/host.tsx +7 -0
- package/src/utils/csv.test.ts +13 -0
- package/src/utils/csv.ts +37 -0
- package/src/utils/save-text-file.test.ts +24 -0
- package/src/utils/save-text-file.ts +36 -0
|
@@ -4,6 +4,8 @@ import { testRender } from "../renderers/opentui/test-utils";
|
|
|
4
4
|
import { AppContext, createInitialState, type AppAction } from "../state/app/context";
|
|
5
5
|
import { cloneLayout, createDefaultConfig } from "../types/config";
|
|
6
6
|
import type { PaneDef } from "../types/plugin";
|
|
7
|
+
import { Box } from "../ui";
|
|
8
|
+
import { PaneFooterBar, PaneFooterProvider } from "../components/layout/pane/footer";
|
|
7
9
|
import type { PluginRegistry } from "../plugins/registry";
|
|
8
10
|
import { LayoutMarketplaceGallery } from "./gallery";
|
|
9
11
|
|
|
@@ -43,7 +45,21 @@ async function renderGallery() {
|
|
|
43
45
|
|
|
44
46
|
testSetup = await testRender(
|
|
45
47
|
<AppContext value={{ state, dispatch: (action) => actions.push(action) }}>
|
|
46
|
-
<
|
|
48
|
+
<PaneFooterProvider>
|
|
49
|
+
{(footer) => (
|
|
50
|
+
<Box width={100} height={24} flexDirection="column">
|
|
51
|
+
<Box width={100} height={23}>
|
|
52
|
+
<LayoutMarketplaceGallery
|
|
53
|
+
pluginRegistry={registry}
|
|
54
|
+
width={100}
|
|
55
|
+
height={23}
|
|
56
|
+
onClose={() => { closed = true; }}
|
|
57
|
+
/>
|
|
58
|
+
</Box>
|
|
59
|
+
<PaneFooterBar footer={footer} focused width={100} />
|
|
60
|
+
</Box>
|
|
61
|
+
)}
|
|
62
|
+
</PaneFooterProvider>
|
|
47
63
|
</AppContext>,
|
|
48
64
|
{ width: 100, height: 24 },
|
|
49
65
|
);
|
|
@@ -65,6 +81,54 @@ test("lists owned layouts before Discover and details the selected layout", asyn
|
|
|
65
81
|
expect(frame).toContain("Portfolio");
|
|
66
82
|
expect(frame).toContain("Ticker Research");
|
|
67
83
|
expect(frame).toContain("3 docked");
|
|
84
|
+
|
|
85
|
+
const searchLine = frame.split("\n").find((line) => line.includes("Search layouts and panes"));
|
|
86
|
+
expect(searchLine?.startsWith("/ Search layouts and panes")).toBe(true);
|
|
87
|
+
expect(frame).toContain("[/]search");
|
|
88
|
+
expect(frame).toContain("[n]ew");
|
|
89
|
+
expect(frame).toContain("[o]pen");
|
|
90
|
+
expect(frame).toContain("[r]ename");
|
|
91
|
+
expect(frame).toContain("[c]opy");
|
|
92
|
+
expect(frame).toContain("[d]elete");
|
|
93
|
+
expect(frame).toContain("[p]ublish");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("search Enter returns to the list before activating the filtered layout", async () => {
|
|
97
|
+
const { actions, isClosed } = await renderGallery();
|
|
98
|
+
|
|
99
|
+
await act(async () => {
|
|
100
|
+
testSetup!.mockInput.pressKey("/");
|
|
101
|
+
await testSetup!.renderOnce();
|
|
102
|
+
});
|
|
103
|
+
await act(async () => {
|
|
104
|
+
await testSetup!.mockInput.typeText("Research");
|
|
105
|
+
testSetup!.mockInput.pressEnter();
|
|
106
|
+
await testSetup!.renderOnce();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(isClosed()).toBe(false);
|
|
110
|
+
expect(actions.some((action) => action.type === "SWITCH_LAYOUT")).toBe(false);
|
|
111
|
+
expect(testSetup!.captureCharFrame()).toContain("/ Research");
|
|
112
|
+
|
|
113
|
+
await act(async () => {
|
|
114
|
+
testSetup!.mockInput.pressEnter();
|
|
115
|
+
await testSetup!.renderOnce();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(actions).toContainEqual({ type: "SWITCH_LAYOUT", index: 1 });
|
|
119
|
+
expect(isClosed()).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("n opens the new layout workflow", async () => {
|
|
123
|
+
const { isClosed } = await renderGallery();
|
|
124
|
+
|
|
125
|
+
await act(async () => {
|
|
126
|
+
testSetup!.mockInput.pressKey("n");
|
|
127
|
+
await testSetup!.renderOnce();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(isClosed()).toBe(false);
|
|
131
|
+
expect(testSetup!.captureCharFrame()).toContain("Create Layout");
|
|
68
132
|
});
|
|
69
133
|
|
|
70
134
|
test("j/k move the selection and Enter switches to the layout and closes", async () => {
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { NormalizedPaneFunctionOptions } from "../../../cli/pane-functions/capabilities";
|
|
2
|
+
import {
|
|
3
|
+
CHART_RESOLUTIONS,
|
|
4
|
+
TIME_RANGES,
|
|
5
|
+
type ChartResolution,
|
|
6
|
+
type TimeRange,
|
|
7
|
+
} from "../../../time-series/range";
|
|
2
8
|
import {
|
|
3
9
|
coerceSeriesTransformForStyle,
|
|
4
10
|
normalizeChartSpec,
|
|
@@ -7,20 +13,17 @@ import type { ChartSeriesSpec, ChartSpec, SeriesPeriod } from "../../../time-ser
|
|
|
7
13
|
|
|
8
14
|
const PRICE_CAPABILITIES = new Set(["price-chart", "intraday-price-chart", "price-comparison"]);
|
|
9
15
|
|
|
10
|
-
function chartRange(options: NormalizedPaneFunctionOptions):
|
|
16
|
+
function chartRange(options: NormalizedPaneFunctionOptions): TimeRange | null {
|
|
11
17
|
const value = options.rangePreset ?? options.range;
|
|
12
|
-
return
|
|
13
|
-
|
|
14
|
-
? value
|
|
18
|
+
return typeof value === "string" && (TIME_RANGES as readonly string[]).includes(value)
|
|
19
|
+
? value as TimeRange
|
|
15
20
|
: null;
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
function chartResolution(options: NormalizedPaneFunctionOptions):
|
|
23
|
+
function chartResolution(options: NormalizedPaneFunctionOptions): ChartResolution | null {
|
|
19
24
|
const value = options.chartResolution ?? options.resolution;
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
|| value === "1wk" || value === "1mo"
|
|
23
|
-
? value
|
|
25
|
+
return typeof value === "string" && (CHART_RESOLUTIONS as readonly string[]).includes(value)
|
|
26
|
+
? value as ChartResolution
|
|
24
27
|
: null;
|
|
25
28
|
}
|
|
26
29
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChartResolution, TimeRange } from "../../../components/chart/core/types";
|
|
2
|
+
import { CHART_RESOLUTIONS, TIME_RANGES as CHART_RANGES } from "../../../time-series/range";
|
|
2
3
|
import type {
|
|
3
4
|
PaneSettingField,
|
|
4
5
|
PaneSettingOption,
|
|
@@ -33,19 +34,7 @@ import {
|
|
|
33
34
|
parseChartSpecOr,
|
|
34
35
|
} from "./chart-spec";
|
|
35
36
|
|
|
36
|
-
export
|
|
37
|
-
export const CHART_RESOLUTIONS: ChartResolution[] = [
|
|
38
|
-
"auto",
|
|
39
|
-
"1m",
|
|
40
|
-
"5m",
|
|
41
|
-
"15m",
|
|
42
|
-
"30m",
|
|
43
|
-
"45m",
|
|
44
|
-
"1h",
|
|
45
|
-
"1d",
|
|
46
|
-
"1wk",
|
|
47
|
-
"1mo",
|
|
48
|
-
];
|
|
37
|
+
export { CHART_RANGES, CHART_RESOLUTIONS };
|
|
49
38
|
|
|
50
39
|
export const CHART_STUDY_OPTIONS: Array<PaneSettingOption & { value: BuiltinStudySelection }> = [
|
|
51
40
|
{ value: "volume", label: "Volume", description: "Volume columns in a lower panel." },
|
|
@@ -29,6 +29,7 @@ import { scannerModule } from "./scanner";
|
|
|
29
29
|
import { sectorsModule } from "./sectors";
|
|
30
30
|
import { treasuryAuctionsModule } from "./treasury-auctions";
|
|
31
31
|
import { worldIndicesModule } from "./world-indices";
|
|
32
|
+
import { worldVenueMapModule } from "./world-venue-map";
|
|
32
33
|
import { yieldCurveModule } from "./yield-curve";
|
|
33
34
|
|
|
34
35
|
const macroSharedResourcesModule = {
|
|
@@ -75,6 +76,7 @@ export const marketOverviewPlugin = composeBuiltinPlugin({
|
|
|
75
76
|
modules: [
|
|
76
77
|
correlationModule,
|
|
77
78
|
worldIndicesModule,
|
|
79
|
+
worldVenueMapModule,
|
|
78
80
|
marketHeatmapModule,
|
|
79
81
|
marketMoversModule,
|
|
80
82
|
marketHaltsModule,
|
|
@@ -250,6 +250,7 @@ export const futuresModule: PluginModule = {
|
|
|
250
250
|
defaultPosition: "right",
|
|
251
251
|
defaultMode: "floating",
|
|
252
252
|
defaultFloatingSize: { width: 76, height: 34 },
|
|
253
|
+
tableExport: true,
|
|
253
254
|
// Resolved per open so the dialog shows the full default set until the
|
|
254
255
|
// user saves a narrower selection.
|
|
255
256
|
settings: (context) => ({
|
|
@@ -169,6 +169,7 @@ export const fxMatrixModule: PluginModule = {
|
|
|
169
169
|
defaultPosition: "right",
|
|
170
170
|
defaultMode: "floating",
|
|
171
171
|
defaultFloatingSize: { width: 105, height: 14 },
|
|
172
|
+
tableExport: true,
|
|
172
173
|
settings: (context) => ({
|
|
173
174
|
title: "FX Cross Rates Settings",
|
|
174
175
|
values: {
|
|
@@ -324,6 +324,7 @@ export const marketMoversModule: PluginModule = {
|
|
|
324
324
|
defaultPosition: "right",
|
|
325
325
|
defaultMode: "floating",
|
|
326
326
|
defaultFloatingSize: { width: 100, height: 36 },
|
|
327
|
+
tableExport: true,
|
|
327
328
|
quickSettings: [LIVE_STREAMING_QUICK_SETTING],
|
|
328
329
|
settings: (context) => withLiveStreamingSetting({
|
|
329
330
|
title: "Market Movers Settings",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import type { OptionContract, OptionsChain, PricePoint } from "../../../types/financials";
|
|
3
|
+
import {
|
|
4
|
+
calculateOptionGreeks,
|
|
5
|
+
calculateOptionsSummary,
|
|
6
|
+
historicalVolatility30d,
|
|
7
|
+
} from "./analytics";
|
|
8
|
+
|
|
9
|
+
function contract(
|
|
10
|
+
strike: number,
|
|
11
|
+
impliedVolatility: number,
|
|
12
|
+
volume: number,
|
|
13
|
+
openInterest: number,
|
|
14
|
+
): OptionContract {
|
|
15
|
+
return {
|
|
16
|
+
contractSymbol: String(strike),
|
|
17
|
+
strike,
|
|
18
|
+
currency: "USD",
|
|
19
|
+
lastPrice: 1,
|
|
20
|
+
change: 0,
|
|
21
|
+
percentChange: 0,
|
|
22
|
+
volume,
|
|
23
|
+
openInterest,
|
|
24
|
+
bid: 0.95,
|
|
25
|
+
ask: 1.05,
|
|
26
|
+
impliedVolatility,
|
|
27
|
+
inTheMoney: false,
|
|
28
|
+
expiration: Date.UTC(2027, 0, 15) / 1000,
|
|
29
|
+
lastTradeDate: 0,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function priceHistory(): PricePoint[] {
|
|
34
|
+
const points: PricePoint[] = [{ date: new Date(Date.UTC(2026, 0, 1)), close: 100 }];
|
|
35
|
+
for (let index = 0; index < 30; index += 1) {
|
|
36
|
+
points.push({
|
|
37
|
+
date: new Date(Date.UTC(2026, 0, index + 2)),
|
|
38
|
+
close: points.at(-1)!.close * Math.exp(index % 2 === 0 ? 0.01 : -0.01),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return points;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test("annualizes the latest 30 daily log returns, including persisted dates", () => {
|
|
45
|
+
const expected = Math.sqrt((30 * 0.01 ** 2 / 29) * 252);
|
|
46
|
+
expect(historicalVolatility30d(priceHistory())).toBeCloseTo(expected, 10);
|
|
47
|
+
expect(historicalVolatility30d(priceHistory().map((point) => ({
|
|
48
|
+
...point,
|
|
49
|
+
date: point.date.toISOString() as unknown as Date,
|
|
50
|
+
})))).toBeCloseTo(expected, 10);
|
|
51
|
+
expect(historicalVolatility30d(priceHistory().slice(1))).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("summarizes the selected expiration without presenting it as whole-chain volume", () => {
|
|
55
|
+
const chain: OptionsChain = {
|
|
56
|
+
underlyingSymbol: "AAPL",
|
|
57
|
+
expirationDates: [Date.UTC(2027, 0, 15) / 1000],
|
|
58
|
+
calls: [contract(100, 0.2, 100, 200), contract(105, 0.3, 200, 400)],
|
|
59
|
+
puts: [contract(100, 0.22, 150, 180), contract(105, 0.4, 300, 420)],
|
|
60
|
+
};
|
|
61
|
+
const summary = calculateOptionsSummary(chain, 101, priceHistory());
|
|
62
|
+
|
|
63
|
+
expect(summary.atmImpliedVolatility).toBeCloseTo(0.21, 10);
|
|
64
|
+
expect(summary.expirationVolume).toBe(750);
|
|
65
|
+
expect(summary.putCallVolumeRatio).toBe(1.5);
|
|
66
|
+
expect(summary.putCallOpenInterestRatio).toBe(1);
|
|
67
|
+
expect(summary.impliedHistoricalRatio).toBeCloseTo(
|
|
68
|
+
summary.atmImpliedVolatility! / summary.historicalVolatility30d!,
|
|
69
|
+
10,
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("derives call and put Greeks from the chain IV", () => {
|
|
74
|
+
const option = contract(100, 0.25, 0, 0);
|
|
75
|
+
const now = Date.UTC(2026, 11, 15);
|
|
76
|
+
const call = calculateOptionGreeks(option, "call", 100, 0, now);
|
|
77
|
+
const put = calculateOptionGreeks(option, "put", 100, 0, now);
|
|
78
|
+
|
|
79
|
+
expect(call?.delta).toBeGreaterThan(0);
|
|
80
|
+
expect(put?.delta).toBeLessThan(0);
|
|
81
|
+
expect(call?.gamma).toBeCloseTo(put!.gamma, 10);
|
|
82
|
+
expect(call?.vegaPerPoint).toBeCloseTo(put!.vegaPerPoint, 10);
|
|
83
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { OptionContract, OptionsChain, PricePoint } from "../../../types/financials";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_OPTION_CALC_DRAFT,
|
|
4
|
+
daysToExpiryFrom,
|
|
5
|
+
valueOption,
|
|
6
|
+
type OptionSide,
|
|
7
|
+
type OptionValuation,
|
|
8
|
+
} from "../options-calculator/model";
|
|
9
|
+
|
|
10
|
+
const TRADING_DAYS_PER_YEAR = 252;
|
|
11
|
+
const HISTORICAL_VOLATILITY_SESSIONS = 30;
|
|
12
|
+
|
|
13
|
+
export interface OptionsSummary {
|
|
14
|
+
atmImpliedVolatility: number | null;
|
|
15
|
+
historicalVolatility30d: number | null;
|
|
16
|
+
impliedHistoricalRatio: number | null;
|
|
17
|
+
expirationVolume: number;
|
|
18
|
+
putCallVolumeRatio: number | null;
|
|
19
|
+
putCallOpenInterestRatio: number | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function positive(value: number | undefined): value is number {
|
|
23
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ratio(numerator: number, denominator: number): number | null {
|
|
27
|
+
return denominator > 0 ? numerator / denominator : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sum(contracts: readonly OptionContract[], field: "volume" | "openInterest"): number {
|
|
31
|
+
return contracts.reduce((total, contract) => {
|
|
32
|
+
const value = contract[field];
|
|
33
|
+
return total + (Number.isFinite(value) && value > 0 ? value : 0);
|
|
34
|
+
}, 0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function atmImpliedVolatility(chain: OptionsChain, spot: number | undefined): number | null {
|
|
38
|
+
if (!positive(spot)) return null;
|
|
39
|
+
const contracts = [...chain.calls, ...chain.puts].filter((contract) => positive(contract.impliedVolatility));
|
|
40
|
+
if (contracts.length === 0) return null;
|
|
41
|
+
const distance = Math.min(...contracts.map((contract) => Math.abs(contract.strike - spot)));
|
|
42
|
+
const atTheMoney = contracts
|
|
43
|
+
.filter((contract) => Math.abs(Math.abs(contract.strike - spot) - distance) < 1e-8)
|
|
44
|
+
.map((contract) => contract.impliedVolatility);
|
|
45
|
+
return atTheMoney.reduce((total, value) => total + value, 0) / atTheMoney.length;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Annualized standard deviation of the latest 30 daily log returns. */
|
|
49
|
+
export function historicalVolatility30d(points: readonly PricePoint[]): number | null {
|
|
50
|
+
const closes = points
|
|
51
|
+
.map((point) => ({
|
|
52
|
+
close: point.close,
|
|
53
|
+
time: point.date instanceof Date ? point.date.getTime() : Date.parse(String(point.date)),
|
|
54
|
+
}))
|
|
55
|
+
.filter((point) => positive(point.close) && Number.isFinite(point.time))
|
|
56
|
+
.sort((a, b) => a.time - b.time)
|
|
57
|
+
.slice(-(HISTORICAL_VOLATILITY_SESSIONS + 1))
|
|
58
|
+
.map((point) => point.close);
|
|
59
|
+
if (closes.length < HISTORICAL_VOLATILITY_SESSIONS + 1) return null;
|
|
60
|
+
|
|
61
|
+
const returns = closes.slice(1).map((close, index) => Math.log(close / closes[index]!));
|
|
62
|
+
const mean = returns.reduce((total, value) => total + value, 0) / returns.length;
|
|
63
|
+
const variance = returns.reduce((total, value) => total + (value - mean) ** 2, 0) / (returns.length - 1);
|
|
64
|
+
return Math.sqrt(variance * TRADING_DAYS_PER_YEAR);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function calculateOptionsSummary(
|
|
68
|
+
chain: OptionsChain,
|
|
69
|
+
spot: number | undefined,
|
|
70
|
+
priceHistory: readonly PricePoint[],
|
|
71
|
+
): OptionsSummary {
|
|
72
|
+
const atmIv = atmImpliedVolatility(chain, spot);
|
|
73
|
+
const historicalVolatility = historicalVolatility30d(priceHistory);
|
|
74
|
+
const callVolume = sum(chain.calls, "volume");
|
|
75
|
+
const putVolume = sum(chain.puts, "volume");
|
|
76
|
+
const callOpenInterest = sum(chain.calls, "openInterest");
|
|
77
|
+
const putOpenInterest = sum(chain.puts, "openInterest");
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
atmImpliedVolatility: atmIv,
|
|
81
|
+
historicalVolatility30d: historicalVolatility,
|
|
82
|
+
impliedHistoricalRatio: atmIv != null && historicalVolatility != null && historicalVolatility > 0
|
|
83
|
+
? atmIv / historicalVolatility
|
|
84
|
+
: null,
|
|
85
|
+
expirationVolume: callVolume + putVolume,
|
|
86
|
+
putCallVolumeRatio: ratio(putVolume, callVolume),
|
|
87
|
+
putCallOpenInterestRatio: ratio(putOpenInterest, callOpenInterest),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function calculateOptionGreeks(
|
|
92
|
+
contract: OptionContract | undefined,
|
|
93
|
+
side: OptionSide,
|
|
94
|
+
spot: number | undefined,
|
|
95
|
+
dividendYield: number | undefined,
|
|
96
|
+
now: number = Date.now(),
|
|
97
|
+
): OptionValuation | undefined {
|
|
98
|
+
if (!contract || !positive(spot) || !positive(contract.strike) || !positive(contract.impliedVolatility)) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
return valueOption({
|
|
102
|
+
...DEFAULT_OPTION_CALC_DRAFT,
|
|
103
|
+
side,
|
|
104
|
+
spot,
|
|
105
|
+
strike: contract.strike,
|
|
106
|
+
daysToExpiry: daysToExpiryFrom(contract.expiration, now),
|
|
107
|
+
volatility: contract.impliedVolatility,
|
|
108
|
+
dividendYield: Number.isFinite(dividendYield) ? dividendYield! : 0,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -6,11 +6,26 @@ import {
|
|
|
6
6
|
LIVE_STREAMING_QUICK_SETTING,
|
|
7
7
|
withLiveStreamingSetting,
|
|
8
8
|
} from "../shared/live-streaming";
|
|
9
|
+
import { OPTION_FIELD_DEFS, resolveOptionFieldIds } from "./table";
|
|
9
10
|
|
|
10
|
-
function optionsSettings(): PaneSettingsDef {
|
|
11
|
+
function optionsSettings(settings: Record<string, unknown>): PaneSettingsDef {
|
|
11
12
|
return {
|
|
12
13
|
title: "Options Settings",
|
|
14
|
+
values: {
|
|
15
|
+
optionColumnIds: resolveOptionFieldIds(settings.optionColumnIds),
|
|
16
|
+
},
|
|
13
17
|
fields: [
|
|
18
|
+
{
|
|
19
|
+
key: "optionColumnIds",
|
|
20
|
+
label: "Columns",
|
|
21
|
+
description: "Choose and order the fields mirrored around the strike.",
|
|
22
|
+
type: "ordered-multi-select",
|
|
23
|
+
options: OPTION_FIELD_DEFS.map((field) => ({
|
|
24
|
+
value: field.id,
|
|
25
|
+
label: field.label,
|
|
26
|
+
description: field.description,
|
|
27
|
+
})),
|
|
28
|
+
},
|
|
14
29
|
{
|
|
15
30
|
key: "chainRefreshMinutes",
|
|
16
31
|
label: "Chain refresh",
|
|
@@ -38,7 +53,7 @@ export const optionsModule: PluginModule = {
|
|
|
38
53
|
defaultMode: "floating",
|
|
39
54
|
defaultFloatingSize: { width: 112, height: 28 },
|
|
40
55
|
quickSettings: [LIVE_STREAMING_QUICK_SETTING],
|
|
41
|
-
settings: (context) => withLiveStreamingSetting(optionsSettings(), context.settings),
|
|
56
|
+
settings: (context) => withLiveStreamingSetting(optionsSettings(context.settings), context.settings),
|
|
42
57
|
},
|
|
43
58
|
],
|
|
44
59
|
|