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
|
@@ -1,32 +1,81 @@
|
|
|
1
1
|
import { TextAttributes } from "../../../ui";
|
|
2
2
|
import type { DataTableCell } from "../../../components";
|
|
3
3
|
import type { OptionContract, OptionsChain } from "../../../types/financials";
|
|
4
|
-
import { blendHex, colors
|
|
4
|
+
import { blendHex, colors } from "../../../theme/colors";
|
|
5
5
|
import { blendForContrast, contrastRatio } from "../../../theme/color-utils";
|
|
6
6
|
import { formatCompact, formatNumber } from "../../../utils/format";
|
|
7
7
|
import { formatMarketPrice } from "../../../market-data/market/format";
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
OptionColumn,
|
|
10
|
+
OptionFieldId,
|
|
11
|
+
OptionTableRow,
|
|
12
|
+
} from "./types";
|
|
13
|
+
import type { OptionSide, OptionValuation } from "../options-calculator/model";
|
|
9
14
|
|
|
10
15
|
type OptionColorRole = "call" | "put" | "price" | "activity" | "iv" | "strike";
|
|
11
16
|
|
|
17
|
+
type OptionFieldDef = {
|
|
18
|
+
id: OptionFieldId;
|
|
19
|
+
label: string;
|
|
20
|
+
header: string;
|
|
21
|
+
width: number;
|
|
22
|
+
description: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
12
25
|
const OPTION_TEXT_MIN_CONTRAST = 4.5;
|
|
13
26
|
|
|
14
|
-
export const
|
|
15
|
-
{ id: "
|
|
16
|
-
{ id: "
|
|
17
|
-
{ id: "
|
|
18
|
-
{ id: "
|
|
19
|
-
{ id: "
|
|
20
|
-
{ id: "
|
|
21
|
-
{ id: "
|
|
22
|
-
{ id: "
|
|
23
|
-
{ id: "
|
|
24
|
-
{ id: "
|
|
25
|
-
{ id: "
|
|
26
|
-
{ id: "putVolume", label: "P VOL", width: 6, align: "right" },
|
|
27
|
-
{ id: "putOpenInterest", label: "P OI", width: 6, align: "right" },
|
|
27
|
+
export const OPTION_FIELD_DEFS: OptionFieldDef[] = [
|
|
28
|
+
{ id: "bid", label: "Bid", header: "BID", width: 7, description: "Best bid price." },
|
|
29
|
+
{ id: "ask", label: "Ask", header: "ASK", width: 7, description: "Best ask price." },
|
|
30
|
+
{ id: "last", label: "Last", header: "LAST", width: 7, description: "Last traded price." },
|
|
31
|
+
{ id: "delta", label: "Delta", header: "Δ", width: 6, description: "Price sensitivity to a $1 move in the underlying." },
|
|
32
|
+
{ id: "gamma", label: "Gamma", header: "Γ", width: 7, description: "Delta sensitivity to a $1 move in the underlying." },
|
|
33
|
+
{ id: "theta", label: "Theta", header: "Θ", width: 7, description: "Estimated value decay per calendar day." },
|
|
34
|
+
{ id: "vega", label: "Vega", header: "VEGA", width: 7, description: "Price sensitivity to one volatility point." },
|
|
35
|
+
{ id: "rho", label: "Rho", header: "RHO", width: 7, description: "Price sensitivity to one interest-rate point." },
|
|
36
|
+
{ id: "iv", label: "Implied volatility", header: "IV", width: 6, description: "Volatility implied by the contract price." },
|
|
37
|
+
{ id: "volume", label: "Volume", header: "VOL", width: 6, description: "Contracts traded in the current session." },
|
|
38
|
+
{ id: "openInterest", label: "Open interest", header: "OI", width: 6, description: "Outstanding open contracts." },
|
|
28
39
|
];
|
|
29
40
|
|
|
41
|
+
export const DEFAULT_OPTION_FIELD_IDS: OptionFieldId[] = ["bid", "ask", "last", "delta", "gamma"];
|
|
42
|
+
|
|
43
|
+
const OPTION_FIELDS_BY_ID = new Map(OPTION_FIELD_DEFS.map((field) => [field.id, field]));
|
|
44
|
+
|
|
45
|
+
export function resolveOptionFieldIds(value: unknown): OptionFieldId[] {
|
|
46
|
+
if (!Array.isArray(value)) return [...DEFAULT_OPTION_FIELD_IDS];
|
|
47
|
+
const seen = new Set<OptionFieldId>();
|
|
48
|
+
const fields = value.filter((id): id is OptionFieldId => {
|
|
49
|
+
if (typeof id !== "string" || !OPTION_FIELDS_BY_ID.has(id as OptionFieldId) || seen.has(id as OptionFieldId)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
seen.add(id as OptionFieldId);
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
return fields.length > 0 ? fields : [...DEFAULT_OPTION_FIELD_IDS];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sideColumn(side: OptionSide, field: OptionFieldId): Omit<OptionColumn, "headerColor"> {
|
|
59
|
+
const definition = OPTION_FIELDS_BY_ID.get(field)!;
|
|
60
|
+
return {
|
|
61
|
+
id: `${side}${field[0]!.toUpperCase()}${field.slice(1)}` as OptionColumn["id"],
|
|
62
|
+
field,
|
|
63
|
+
side,
|
|
64
|
+
label: `${side === "call" ? "C" : "P"} ${definition.header}`,
|
|
65
|
+
width: definition.width,
|
|
66
|
+
align: "right",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createOptionColumns(fieldIds: readonly OptionFieldId[]): Array<Omit<OptionColumn, "headerColor">> {
|
|
71
|
+
const fields = resolveOptionFieldIds(fieldIds);
|
|
72
|
+
return [
|
|
73
|
+
...fields.map((field) => sideColumn("call", field)),
|
|
74
|
+
{ id: "strike", field: "strike", side: null, label: "STRIKE", width: 9, align: "right" },
|
|
75
|
+
...[...fields].reverse().map((field) => sideColumn("put", field)),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
30
79
|
export function buildStrikeList(chain: OptionsChain): number[] {
|
|
31
80
|
const set = new Set<number>();
|
|
32
81
|
for (const c of chain.calls) set.add(c.strike);
|
|
@@ -68,20 +117,32 @@ export function formatIv(value: number | undefined): string {
|
|
|
68
117
|
return `${(value * 100).toFixed(1)}%`;
|
|
69
118
|
}
|
|
70
119
|
|
|
71
|
-
|
|
72
|
-
|
|
120
|
+
function formatGreek(value: number | undefined): string {
|
|
121
|
+
if (value == null || !Number.isFinite(value)) return "\u2014";
|
|
122
|
+
return value.toFixed(3).replace(/^(-?)0\./, "$1.");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function optionColumnColor(
|
|
126
|
+
column: Pick<OptionColumn, "field" | "side">,
|
|
127
|
+
surface = colors.bg,
|
|
128
|
+
): string {
|
|
129
|
+
return optionRoleColor(optionColumnRole(column), surface);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function optionContractForColumn(row: OptionTableRow, column: OptionColumn): OptionContract | undefined {
|
|
133
|
+
return column.side === "call" ? row.call : row.put;
|
|
73
134
|
}
|
|
74
135
|
|
|
75
|
-
function
|
|
76
|
-
return
|
|
136
|
+
function optionGreeksForColumn(row: OptionTableRow, column: OptionColumn): OptionValuation | undefined {
|
|
137
|
+
return column.side === "call" ? row.callGreeks : row.putGreeks;
|
|
77
138
|
}
|
|
78
139
|
|
|
79
|
-
function optionColumnRole(
|
|
80
|
-
if (
|
|
81
|
-
if (
|
|
82
|
-
if (
|
|
83
|
-
if (
|
|
84
|
-
return
|
|
140
|
+
function optionColumnRole(column: Pick<OptionColumn, "field" | "side">): OptionColorRole {
|
|
141
|
+
if (column.field === "strike") return "strike";
|
|
142
|
+
if (column.field === "iv") return "iv";
|
|
143
|
+
if (column.field === "volume" || column.field === "openInterest") return "activity";
|
|
144
|
+
if (column.field === "bid" || column.field === "ask") return "price";
|
|
145
|
+
return column.side ?? "strike";
|
|
85
146
|
}
|
|
86
147
|
|
|
87
148
|
function optionRoleThemeColor(role: OptionColorRole): string {
|
|
@@ -131,12 +192,12 @@ function optionMutedColor(surface: string): string {
|
|
|
131
192
|
function optionMoneynessBackground(
|
|
132
193
|
row: OptionTableRow,
|
|
133
194
|
contract: OptionContract | undefined,
|
|
134
|
-
|
|
195
|
+
column: OptionColumn,
|
|
135
196
|
rowState: { selected: boolean },
|
|
136
197
|
): string | undefined {
|
|
137
|
-
if (rowState.selected) return undefined;
|
|
138
|
-
const inTheMoney = inferColumnMoneyness(row, contract,
|
|
139
|
-
const sideColor =
|
|
198
|
+
if (rowState.selected || !column.side) return undefined;
|
|
199
|
+
const inTheMoney = inferColumnMoneyness(row, contract, column.side);
|
|
200
|
+
const sideColor = column.side === "call" ? colors.positive : colors.negative;
|
|
140
201
|
return inTheMoney
|
|
141
202
|
? blendHex(colors.bg, sideColor, 0.13)
|
|
142
203
|
: blendHex(colors.bg, colors.neutral, 0.055);
|
|
@@ -145,34 +206,43 @@ function optionMoneynessBackground(
|
|
|
145
206
|
function inferColumnMoneyness(
|
|
146
207
|
row: OptionTableRow,
|
|
147
208
|
contract: OptionContract | undefined,
|
|
148
|
-
|
|
209
|
+
side: OptionSide,
|
|
149
210
|
): boolean {
|
|
150
211
|
if (contract) return contract.inTheMoney;
|
|
151
|
-
const oppositeContract =
|
|
212
|
+
const oppositeContract = side === "call" ? row.put : row.call;
|
|
152
213
|
return oppositeContract ? !oppositeContract.inTheMoney : false;
|
|
153
214
|
}
|
|
154
215
|
|
|
155
|
-
function formatOptionContractCell(
|
|
216
|
+
function formatOptionContractCell(
|
|
217
|
+
row: OptionTableRow,
|
|
218
|
+
contract: OptionContract | undefined,
|
|
219
|
+
column: OptionColumn,
|
|
220
|
+
): string {
|
|
156
221
|
if (!contract) return "—";
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
case "
|
|
222
|
+
const greeks = optionGreeksForColumn(row, column);
|
|
223
|
+
switch (column.field) {
|
|
224
|
+
case "last":
|
|
160
225
|
return formatMarketPrice(contract.lastPrice, { assetCategory: "OPT", maxWidth: column.width });
|
|
161
|
-
case "
|
|
162
|
-
case "putBid":
|
|
226
|
+
case "bid":
|
|
163
227
|
return formatMarketPrice(contract.bid, { assetCategory: "OPT", maxWidth: column.width });
|
|
164
|
-
case "
|
|
165
|
-
case "putAsk":
|
|
228
|
+
case "ask":
|
|
166
229
|
return formatMarketPrice(contract.ask, { assetCategory: "OPT", maxWidth: column.width });
|
|
167
|
-
case "
|
|
168
|
-
case "putVolume":
|
|
230
|
+
case "volume":
|
|
169
231
|
return formatCompact(contract.volume);
|
|
170
|
-
case "
|
|
171
|
-
case "putOpenInterest":
|
|
232
|
+
case "openInterest":
|
|
172
233
|
return formatCompact(contract.openInterest);
|
|
173
|
-
case "
|
|
174
|
-
case "putIv":
|
|
234
|
+
case "iv":
|
|
175
235
|
return formatIv(contract.impliedVolatility);
|
|
236
|
+
case "delta":
|
|
237
|
+
return formatGreek(greeks?.delta);
|
|
238
|
+
case "gamma":
|
|
239
|
+
return formatGreek(greeks?.gamma);
|
|
240
|
+
case "theta":
|
|
241
|
+
return formatGreek(greeks?.thetaPerDay);
|
|
242
|
+
case "vega":
|
|
243
|
+
return formatGreek(greeks?.vegaPerPoint);
|
|
244
|
+
case "rho":
|
|
245
|
+
return formatGreek(greeks?.rhoPerPoint);
|
|
176
246
|
case "strike":
|
|
177
247
|
return formatStrikeLabel(contract.strike);
|
|
178
248
|
}
|
|
@@ -187,7 +257,7 @@ export function renderOptionCell(
|
|
|
187
257
|
const selectedColor = rowState.selected ? colors.selectedText : undefined;
|
|
188
258
|
const rowSurface = rowState.selected ? colors.selected : colors.bg;
|
|
189
259
|
|
|
190
|
-
if (column.
|
|
260
|
+
if (column.field === "strike") {
|
|
191
261
|
const backgroundColor = rowState.selected
|
|
192
262
|
? undefined
|
|
193
263
|
: blendHex(colors.bg, row.isPositionStrike ? colors.borderFocused : colors.header, row.isPositionStrike ? 0.18 : 0.1);
|
|
@@ -200,12 +270,12 @@ export function renderOptionCell(
|
|
|
200
270
|
};
|
|
201
271
|
}
|
|
202
272
|
|
|
203
|
-
const contract = optionContractForColumn(row, column
|
|
204
|
-
const backgroundColor = optionMoneynessBackground(row, contract, column
|
|
273
|
+
const contract = optionContractForColumn(row, column);
|
|
274
|
+
const backgroundColor = optionMoneynessBackground(row, contract, column, rowState);
|
|
205
275
|
const surface = backgroundColor ?? rowSurface;
|
|
206
276
|
return {
|
|
207
|
-
text: formatOptionContractCell(contract, column),
|
|
208
|
-
color: selectedColor ?? (contract ? optionRoleColor(optionColumnRole(column
|
|
277
|
+
text: formatOptionContractCell(row, contract, column),
|
|
278
|
+
color: selectedColor ?? (contract ? optionRoleColor(optionColumnRole(column), surface) : optionMutedColor(surface)),
|
|
209
279
|
backgroundColor,
|
|
210
280
|
};
|
|
211
281
|
}
|
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
import type { DataTableColumn } from "../../../components";
|
|
2
2
|
import type { OptionContract } from "../../../types/financials";
|
|
3
|
+
import type { OptionSide, OptionValuation } from "../options-calculator/model";
|
|
3
4
|
|
|
4
|
-
export type
|
|
5
|
-
| "
|
|
6
|
-
| "
|
|
7
|
-
| "
|
|
8
|
-
| "
|
|
9
|
-
| "
|
|
10
|
-
| "
|
|
11
|
-
| "
|
|
12
|
-
| "
|
|
13
|
-
| "
|
|
14
|
-
| "
|
|
15
|
-
| "
|
|
16
|
-
| "putOpenInterest"
|
|
17
|
-
| "putIv";
|
|
5
|
+
export type OptionFieldId =
|
|
6
|
+
| "bid"
|
|
7
|
+
| "ask"
|
|
8
|
+
| "last"
|
|
9
|
+
| "delta"
|
|
10
|
+
| "gamma"
|
|
11
|
+
| "theta"
|
|
12
|
+
| "vega"
|
|
13
|
+
| "rho"
|
|
14
|
+
| "iv"
|
|
15
|
+
| "volume"
|
|
16
|
+
| "openInterest";
|
|
18
17
|
|
|
19
|
-
export type
|
|
18
|
+
export type OptionColumnId = "strike" | `${OptionSide}${Capitalize<OptionFieldId>}`;
|
|
19
|
+
|
|
20
|
+
export type OptionColumn = DataTableColumn & {
|
|
21
|
+
id: OptionColumnId;
|
|
22
|
+
field: OptionFieldId | "strike";
|
|
23
|
+
side: OptionSide | null;
|
|
24
|
+
};
|
|
20
25
|
|
|
21
26
|
export interface OptionTableRow {
|
|
22
27
|
strike: number;
|
|
23
28
|
call?: OptionContract;
|
|
24
29
|
put?: OptionContract;
|
|
30
|
+
callGreeks?: OptionValuation;
|
|
31
|
+
putGreeks?: OptionValuation;
|
|
25
32
|
isPositionStrike: boolean;
|
|
26
33
|
}
|
|
27
34
|
|
|
@@ -176,6 +176,30 @@ test("defaults the table around the nearest strike to the current quote", async
|
|
|
176
176
|
expect(frame).not.toContain(" 50 ");
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
+
test("shows volatility statistics and mirrored default Greeks", async () => {
|
|
180
|
+
const provider = createTestDataProvider({
|
|
181
|
+
getOptionsChain: async () => makeChain([100, 101], 101),
|
|
182
|
+
});
|
|
183
|
+
setSharedMarketDataCoordinator(new MarketDataCoordinator(provider));
|
|
184
|
+
|
|
185
|
+
await act(async () => {
|
|
186
|
+
testSetup = await testRender(
|
|
187
|
+
<OptionsHarness ticker={makeTicker("AAPL")} quotePrice={101} />,
|
|
188
|
+
{ width: 124, height: 16 },
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
await renderSettled();
|
|
192
|
+
|
|
193
|
+
const frame = testSetup!.captureCharFrame();
|
|
194
|
+
expect(frame).toContain("ATM IV 20.0%");
|
|
195
|
+
expect(frame).toContain("HV30 —");
|
|
196
|
+
expect(frame).toContain("EXP VOL");
|
|
197
|
+
expect(frame).toContain("C Δ");
|
|
198
|
+
expect(frame).toContain("C Γ");
|
|
199
|
+
expect(frame).toContain("P Γ");
|
|
200
|
+
expect(frame).toContain("P Δ");
|
|
201
|
+
});
|
|
202
|
+
|
|
179
203
|
test("streams live quotes without resetting manual scroll", async () => {
|
|
180
204
|
const strikes = Array.from({ length: 100 }, (_, index) => 50 + index);
|
|
181
205
|
const subscriptions: QuoteSubscriptionTarget[][] = [];
|
|
@@ -3,6 +3,7 @@ import { Box, Text } from "../../../ui";
|
|
|
3
3
|
import { usePaneSettingValue, usePaneTicker } from "../../../state/app/context";
|
|
4
4
|
import { colors } from "../../../theme/colors";
|
|
5
5
|
import { isPlainKey } from "../../../utils/keyboard";
|
|
6
|
+
import { formatCompact } from "../../../utils/format";
|
|
6
7
|
import { formatExpDate, resolveOptionsTarget } from "../../../utils/options";
|
|
7
8
|
import { useOptionsQuery, useResolvedEntryValue, useTickerFinancials } from "../../../market-data/hooks";
|
|
8
9
|
import {
|
|
@@ -21,15 +22,19 @@ import {
|
|
|
21
22
|
type OptionSide,
|
|
22
23
|
} from "../options-calculator/model";
|
|
23
24
|
import { buildChainCalcParams, resolveCalcSide } from "./calc-seed";
|
|
25
|
+
import { calculateOptionGreeks, calculateOptionsSummary, type OptionsSummary } from "./analytics";
|
|
24
26
|
import {
|
|
25
|
-
|
|
27
|
+
DEFAULT_OPTION_FIELD_IDS,
|
|
26
28
|
buildStrikeList,
|
|
29
|
+
createOptionColumns,
|
|
27
30
|
findNearestStrikeIndex,
|
|
31
|
+
formatIv,
|
|
28
32
|
optionColumnColor,
|
|
29
33
|
renderOptionCell,
|
|
30
34
|
resolveDefaultStrikeTarget,
|
|
35
|
+
resolveOptionFieldIds,
|
|
31
36
|
} from "./table";
|
|
32
|
-
import type { OptionColumn, OptionTableRow, OptionsViewProps } from "./types";
|
|
37
|
+
import type { OptionColumn, OptionFieldId, OptionTableRow, OptionsViewProps } from "./types";
|
|
33
38
|
import {
|
|
34
39
|
buildOptionQuoteTargets,
|
|
35
40
|
overlayOptionRowQuotes,
|
|
@@ -39,6 +44,48 @@ import {
|
|
|
39
44
|
import { useOptionsAccessFooter } from "./footer";
|
|
40
45
|
import { useLiveStreamingSetting } from "../shared/live-streaming";
|
|
41
46
|
|
|
47
|
+
type SummaryMetric = { label: string; value: string };
|
|
48
|
+
|
|
49
|
+
function formatRatio(value: number | null | undefined): string {
|
|
50
|
+
return value == null || !Number.isFinite(value) ? "—" : value.toFixed(2);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function SummaryRow({ metrics }: { metrics: SummaryMetric[] }) {
|
|
54
|
+
return (
|
|
55
|
+
<Box flexDirection="row" height={1} gap={3} overflow="hidden">
|
|
56
|
+
{metrics.map((metric) => (
|
|
57
|
+
<Box key={metric.label} flexDirection="row" flexShrink={0}>
|
|
58
|
+
<Text fg={colors.textDim}>{`${metric.label} `}</Text>
|
|
59
|
+
<Text fg={colors.textBright}>{metric.value}</Text>
|
|
60
|
+
</Box>
|
|
61
|
+
))}
|
|
62
|
+
</Box>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function OptionsSummaryStrip({ summary, secondary }: {
|
|
67
|
+
summary: OptionsSummary | null;
|
|
68
|
+
secondary: boolean;
|
|
69
|
+
}) {
|
|
70
|
+
const volatility: SummaryMetric[] = [
|
|
71
|
+
{ label: "ATM IV", value: formatIv(summary?.atmImpliedVolatility ?? undefined) },
|
|
72
|
+
{ label: "HV30", value: formatIv(summary?.historicalVolatility30d ?? undefined) },
|
|
73
|
+
{ label: "IV/HV", value: formatRatio(summary?.impliedHistoricalRatio) },
|
|
74
|
+
];
|
|
75
|
+
return (
|
|
76
|
+
<Box flexDirection="column" height={secondary ? 2 : 1}>
|
|
77
|
+
<SummaryRow metrics={volatility} />
|
|
78
|
+
{secondary && (
|
|
79
|
+
<SummaryRow metrics={[
|
|
80
|
+
{ label: "EXP VOL", value: summary ? formatCompact(summary.expirationVolume) : "—" },
|
|
81
|
+
{ label: "P/C VOL", value: formatRatio(summary?.putCallVolumeRatio) },
|
|
82
|
+
{ label: "P/C OI", value: formatRatio(summary?.putCallOpenInterestRatio) },
|
|
83
|
+
]} />
|
|
84
|
+
)}
|
|
85
|
+
</Box>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
42
89
|
export function OptionsView({ width, height, focused, onCapture = () => {} }: OptionsViewProps) {
|
|
43
90
|
const { ticker, financials } = usePaneTicker();
|
|
44
91
|
const { createPaneFromTemplate } = usePluginAppActions();
|
|
@@ -61,6 +108,9 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
61
108
|
const effectiveTicker = target?.effectiveTicker ?? "";
|
|
62
109
|
const effectiveExchange = target?.effectiveExchange ?? "";
|
|
63
110
|
const underlyingFinancials = useTickerFinancials(isOpt ? effectiveTicker : null, null);
|
|
111
|
+
const underlying = isOpt ? underlyingFinancials : financials;
|
|
112
|
+
const spot = underlying?.quote?.price;
|
|
113
|
+
const dividendYield = underlying?.fundamentals?.dividendYield;
|
|
64
114
|
const instrument = target?.instrument ?? null;
|
|
65
115
|
const baseRequest = target
|
|
66
116
|
? {
|
|
@@ -74,6 +124,8 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
74
124
|
}
|
|
75
125
|
: null;
|
|
76
126
|
const [chainRefreshMinutes] = usePaneSettingValue<string>("chainRefreshMinutes", "");
|
|
127
|
+
const [storedOptionFieldIds] = usePaneSettingValue<OptionFieldId[]>("optionColumnIds", DEFAULT_OPTION_FIELD_IDS);
|
|
128
|
+
const optionFieldIds = useMemo(() => resolveOptionFieldIds(storedOptionFieldIds), [storedOptionFieldIds]);
|
|
77
129
|
const initialChainEntry = useOptionsQuery(baseRequest);
|
|
78
130
|
const initialChain = useResolvedEntryValue(initialChainEntry);
|
|
79
131
|
const selectedExpiration = initialChain?.expirationDates[expIdx];
|
|
@@ -157,12 +209,27 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
157
209
|
() => new Map(strikeChain?.puts.map((p) => [p.strike, p]) ?? []),
|
|
158
210
|
[strikeChain],
|
|
159
211
|
);
|
|
160
|
-
const snapshotRows = useMemo<OptionTableRow[]>(() =>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
212
|
+
const snapshotRows = useMemo<OptionTableRow[]>(() => {
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
return strikes.map((strike) => {
|
|
215
|
+
const call = callsByStrike.get(strike);
|
|
216
|
+
const put = putsByStrike.get(strike);
|
|
217
|
+
return {
|
|
218
|
+
strike,
|
|
219
|
+
call,
|
|
220
|
+
put,
|
|
221
|
+
callGreeks: calculateOptionGreeks(call, "call", spot, dividendYield, now),
|
|
222
|
+
putGreeks: calculateOptionGreeks(put, "put", spot, dividendYield, now),
|
|
223
|
+
isPositionStrike: !!parsed && Math.abs(strike - parsed.strike) < 0.01,
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
}, [callsByStrike, dividendYield, parsed, putsByStrike, spot, strikes]);
|
|
227
|
+
const summary = useMemo(
|
|
228
|
+
() => strikeChain
|
|
229
|
+
? calculateOptionsSummary(strikeChain, spot, underlying?.priceHistory ?? [])
|
|
230
|
+
: null,
|
|
231
|
+
[spot, strikeChain, underlying?.priceHistory],
|
|
232
|
+
);
|
|
166
233
|
const visibleStrikeRange = visibleStrikeViewport?.key === viewportKey
|
|
167
234
|
? visibleStrikeViewport.range
|
|
168
235
|
: null;
|
|
@@ -210,10 +277,10 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
210
277
|
),
|
|
211
278
|
[optionQuoteEntries, optionQuoteFreshness, optionQuoteTargets],
|
|
212
279
|
);
|
|
213
|
-
const optionColumns
|
|
280
|
+
const optionColumns = useMemo<OptionColumn[]>(() => createOptionColumns(optionFieldIds).map((column) => ({
|
|
214
281
|
...column,
|
|
215
|
-
headerColor: optionColumnColor(column
|
|
216
|
-
}));
|
|
282
|
+
headerColor: optionColumnColor(column, colors.panel),
|
|
283
|
+
})), [optionFieldIds]);
|
|
217
284
|
|
|
218
285
|
const selectedRow = rows[strikeIdx] ?? null;
|
|
219
286
|
const calcParams = useMemo(() => buildChainCalcParams({
|
|
@@ -222,9 +289,9 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
222
289
|
side: resolveCalcSide(calcSide, parsed?.side, selectedRow),
|
|
223
290
|
// On an option ticker the pane quote is the contract's own price, so load
|
|
224
291
|
// the underlying snapshot rather than silently using the option mark as spot.
|
|
225
|
-
spot
|
|
226
|
-
dividendYield
|
|
227
|
-
}), [calcSide,
|
|
292
|
+
spot,
|
|
293
|
+
dividendYield,
|
|
294
|
+
}), [calcSide, dividendYield, effectiveTicker, parsed?.side, selectedRow, spot]);
|
|
228
295
|
|
|
229
296
|
const openCalculator = useCallback(() => {
|
|
230
297
|
if (!calcParams) return;
|
|
@@ -243,10 +310,10 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
243
310
|
rowState: { selected: boolean },
|
|
244
311
|
) => {
|
|
245
312
|
const cell = renderOptionCell(row, column, index, rowState);
|
|
246
|
-
if (column.
|
|
313
|
+
if (!column.side) return cell;
|
|
247
314
|
// Clicking a call or put cell is the mouse way to choose which contract
|
|
248
315
|
// [c]alc opens, so it has to select the row itself as well.
|
|
249
|
-
const side: OptionSide = column.
|
|
316
|
+
const side: OptionSide = column.side;
|
|
250
317
|
return {
|
|
251
318
|
...cell,
|
|
252
319
|
onMouseDown: () => {
|
|
@@ -277,12 +344,12 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
277
344
|
|
|
278
345
|
useEffect(() => {
|
|
279
346
|
if (strikes.length === 0 || userSelectedStrikeRef.current) return;
|
|
280
|
-
const targetStrike = resolveDefaultStrikeTarget(parsed?.strike,
|
|
347
|
+
const targetStrike = resolveDefaultStrikeTarget(parsed?.strike, spot);
|
|
281
348
|
if (targetStrike == null) return;
|
|
282
349
|
setScrollToIndexAlign("center");
|
|
283
350
|
setStrikeIdx(findNearestStrikeIndex(strikes, targetStrike));
|
|
284
351
|
setAutoScrollVersion((version) => version + 1);
|
|
285
|
-
}, [expIdx,
|
|
352
|
+
}, [expIdx, parsed?.strike, spot, strikes]);
|
|
286
353
|
|
|
287
354
|
useShortcut((event) => {
|
|
288
355
|
if (event.defaultPrevented || event.propagationStopped || event.targetEditable) return;
|
|
@@ -399,13 +466,18 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
399
466
|
? ticker.metadata.positions.reduce((sum, p) => sum + p.shares, 0)
|
|
400
467
|
: 0;
|
|
401
468
|
const expirationTabsWidth = Math.max(width - 9 - (loading ? 2 : 0), 8);
|
|
402
|
-
const
|
|
469
|
+
const summaryRowCount = height >= 10 ? 2 : height >= 7 ? 1 : 0;
|
|
470
|
+
const tableHeight = Math.max(1, height - 1 - summaryRowCount - (isOpt && parsed ? 1 : 0));
|
|
403
471
|
// The strip scrolls; without a marker a clipped last date reads as the last expiry.
|
|
404
472
|
const expirationStripOverflows = chain.expirationDates
|
|
405
473
|
.reduce((total, ts) => total + formatExpDate(ts).length + 2, 0) > expirationTabsWidth;
|
|
406
474
|
|
|
407
475
|
return (
|
|
408
476
|
<Box flexDirection="column" flexGrow={1} paddingX={1} onMouseDown={() => { if (!interactive) enterInteractive(); }}>
|
|
477
|
+
{summaryRowCount > 0 && (
|
|
478
|
+
<OptionsSummaryStrip summary={summary} secondary={summaryRowCount > 1} />
|
|
479
|
+
)}
|
|
480
|
+
|
|
409
481
|
<Box flexDirection="row" height={1} gap={1}>
|
|
410
482
|
<Text fg={colors.textDim}>Exp:</Text>
|
|
411
483
|
<Box width={expirationTabsWidth} height={1} overflow="hidden">
|
|
@@ -19,8 +19,7 @@ describe("composeBuiltinPlugin", () => {
|
|
|
19
19
|
test("combines every declarative contribution under the parent plugin", async () => {
|
|
20
20
|
const registeredBrokers: string[] = [];
|
|
21
21
|
const first: PluginModule = {
|
|
22
|
-
cliCommands: [{ name: "
|
|
23
|
-
cli: { commands: [{ name: "typed", summary: "Typed" }] },
|
|
22
|
+
cliCommands: [{ name: "example", description: "Example", execute: () => {} }],
|
|
24
23
|
panes: [{ id: "one", name: "One", component: () => null, defaultPosition: "right" }],
|
|
25
24
|
paneTemplates: [{ id: "one-pane", paneId: "one", label: "One", description: "One" }],
|
|
26
25
|
capabilities: [{ id: "capability-one" } as never],
|
|
@@ -40,8 +39,7 @@ describe("composeBuiltinPlugin", () => {
|
|
|
40
39
|
modules: [first, second],
|
|
41
40
|
});
|
|
42
41
|
|
|
43
|
-
expect(plugin.cliCommands?.map((command) => command.name)).toEqual(["
|
|
44
|
-
expect(plugin.cli?.commands?.map((command) => command.name)).toEqual(["typed"]);
|
|
42
|
+
expect(plugin.cliCommands?.map((command) => command.name)).toEqual(["example"]);
|
|
45
43
|
expect(plugin.panes?.map((pane) => pane.id)).toEqual(["one", "two"]);
|
|
46
44
|
expect(plugin.paneTemplates?.map((template) => template.id)).toEqual(["one-pane"]);
|
|
47
45
|
expect(plugin.capabilities?.map((capability) => capability.id)).toEqual(["capability-one"]);
|
|
@@ -2,7 +2,6 @@ import { Fragment, createElement, type ReactNode } from "react";
|
|
|
2
2
|
import type {
|
|
3
3
|
GloomPlugin,
|
|
4
4
|
GloomPluginContext,
|
|
5
|
-
PluginCliDescriptor,
|
|
6
5
|
} from "../../types/plugin";
|
|
7
6
|
|
|
8
7
|
type PluginMetadataKey = "id" | "name" | "version" | "description" | "toggleable" | "order";
|
|
@@ -12,7 +11,6 @@ export type PluginModule = Omit<GloomPlugin, PluginMetadataKey>;
|
|
|
12
11
|
|
|
13
12
|
const HANDLED_MODULE_KEYS = [
|
|
14
13
|
"cliCommands",
|
|
15
|
-
"cli",
|
|
16
14
|
"setup",
|
|
17
15
|
"dispose",
|
|
18
16
|
"panes",
|
|
@@ -23,11 +21,8 @@ const HANDLED_MODULE_KEYS = [
|
|
|
23
21
|
] as const satisfies readonly (keyof PluginModule)[];
|
|
24
22
|
|
|
25
23
|
type MissingModuleKey = Exclude<keyof PluginModule, typeof HANDLED_MODULE_KEYS[number]>;
|
|
26
|
-
type MissingCliDescriptorKey = Exclude<keyof PluginCliDescriptor, "commands">;
|
|
27
24
|
const ALL_MODULE_KEYS_HANDLED: MissingModuleKey extends never ? true : never = true;
|
|
28
|
-
const ALL_CLI_DESCRIPTOR_KEYS_HANDLED: MissingCliDescriptorKey extends never ? true : never = true;
|
|
29
25
|
void ALL_MODULE_KEYS_HANDLED;
|
|
30
|
-
void ALL_CLI_DESCRIPTOR_KEYS_HANDLED;
|
|
31
26
|
|
|
32
27
|
interface CompositePluginOptions extends PluginMetadata {
|
|
33
28
|
modules: readonly PluginModule[];
|
|
@@ -72,7 +67,6 @@ function composeSlots(modules: readonly PluginModule[]): GloomPlugin["slots"] {
|
|
|
72
67
|
export function composeBuiltinPlugin(options: CompositePluginOptions): GloomPlugin {
|
|
73
68
|
const { modules, ...metadata } = options;
|
|
74
69
|
const cliCommands = modules.flatMap((module) => module.cliCommands ?? []);
|
|
75
|
-
const cliDescriptors = modules.flatMap((module) => module.cli?.commands ?? []);
|
|
76
70
|
const panes = modules.flatMap((module) => module.panes ?? []);
|
|
77
71
|
const paneTemplates = modules.flatMap((module) => module.paneTemplates ?? []);
|
|
78
72
|
const capabilities = modules.flatMap((module) => module.capabilities ?? []);
|
|
@@ -83,7 +77,6 @@ export function composeBuiltinPlugin(options: CompositePluginOptions): GloomPlug
|
|
|
83
77
|
return {
|
|
84
78
|
...metadata,
|
|
85
79
|
...(cliCommands.length > 0 ? { cliCommands } : {}),
|
|
86
|
-
...(cliDescriptors.length > 0 ? { cli: { commands: cliDescriptors } } : {}),
|
|
87
80
|
...(panes.length > 0 ? { panes } : {}),
|
|
88
81
|
...(paneTemplates.length > 0 ? { paneTemplates } : {}),
|
|
89
82
|
...(capabilities.length > 0 ? { capabilities } : {}),
|
|
@@ -72,6 +72,7 @@ export const scannerModule: PluginModule = {
|
|
|
72
72
|
defaultPosition: "right",
|
|
73
73
|
defaultMode: "floating",
|
|
74
74
|
defaultFloatingSize: { width: 76, height: 26 },
|
|
75
|
+
tableExport: true,
|
|
75
76
|
settings: hiloSettings(),
|
|
76
77
|
},
|
|
77
78
|
{
|
|
@@ -82,6 +83,7 @@ export const scannerModule: PluginModule = {
|
|
|
82
83
|
defaultPosition: "right",
|
|
83
84
|
defaultMode: "floating",
|
|
84
85
|
defaultFloatingSize: { width: 88, height: 28 },
|
|
86
|
+
tableExport: true,
|
|
85
87
|
settings: flowSettings(),
|
|
86
88
|
},
|
|
87
89
|
],
|
|
@@ -320,6 +320,7 @@ export const sectorsModule: PluginModule = {
|
|
|
320
320
|
defaultPosition: "right",
|
|
321
321
|
defaultMode: "floating",
|
|
322
322
|
defaultFloatingSize: { width: 82, height: 18 },
|
|
323
|
+
tableExport: true,
|
|
323
324
|
settings: (context) => ({
|
|
324
325
|
title: "Sector Performance Settings",
|
|
325
326
|
values: {
|