diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5029b2 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +Choose Underlying +Choose Strike +For each front-mont-back-month combination: + Lookup the cost to open the position + At the front month's expiration, the back-month will have a certain DTE. Determine what IV the back-month will need in order to offset this cost. + Determine the 30-day lo-hi range for IV, normalized for distance-from-the-money and time-to-expiry. + In other words, a naive 30-day-lo-hi isn't informative, because maybe it was low due to long time-to-expiry, or high due to distance-from-the-money + Normalize the IV that was determined to yield a profit, and see if it's higher than the bottom of the lo-hi range. If it is, it's safe; the only way to lose is for it to end-off having a lower IV than the 30-day record. \ No newline at end of file diff --git a/dist/App.module.css b/dist/App.module.css index 18a0e35..8aab74e 100644 --- a/dist/App.module.css +++ b/dist/App.module.css @@ -3,7 +3,18 @@ flex-direction: column; display: flex; } -.App-module__app_gPMrEW__001 > .App-module__picker_gPMrEW__001 { +.App-module__app_gPMrEW__001 > .App-module__form_gPMrEW__001 { + flex-direction: column; + max-width: 30em; + display: flex; +} +.App-module__app_gPMrEW__001 > .App-module__form_gPMrEW__001 > .App-module__underlyingPrice_gPMrEW__001 { + flex-direction: row; + justify-content: flex-start; + gap: .6em; + display: flex; +} +.App-module__app_gPMrEW__001 > .App-module__form_gPMrEW__001 > .App-module__picker_gPMrEW__001 { flex-direction: row; justify-content: space-between; display: flex; diff --git a/dist/App.module.js b/dist/App.module.js index 7897149..21edfd6 100644 --- a/dist/App.module.js +++ b/dist/App.module.js @@ -1,7 +1,9 @@ // src/App.module.css var App_module_default = { "app": "App-module__app_gPMrEW__001", - "picker": "App-module__picker_gPMrEW__001" + "form": "App-module__form_gPMrEW__001", + "picker": "App-module__picker_gPMrEW__001", + "underlyingPrice": "App-module__underlyingPrice_gPMrEW__001" }; export { App_module_default as default diff --git a/dist/index.css b/dist/index.css index 18a0e35..8aab74e 100644 --- a/dist/index.css +++ b/dist/index.css @@ -3,7 +3,18 @@ flex-direction: column; display: flex; } -.App-module__app_gPMrEW__001 > .App-module__picker_gPMrEW__001 { +.App-module__app_gPMrEW__001 > .App-module__form_gPMrEW__001 { + flex-direction: column; + max-width: 30em; + display: flex; +} +.App-module__app_gPMrEW__001 > .App-module__form_gPMrEW__001 > .App-module__underlyingPrice_gPMrEW__001 { + flex-direction: row; + justify-content: flex-start; + gap: .6em; + display: flex; +} +.App-module__app_gPMrEW__001 > .App-module__form_gPMrEW__001 > .App-module__picker_gPMrEW__001 { flex-direction: row; justify-content: space-between; display: flex; diff --git a/dist/index.js b/dist/index.js index fb3ac3d..3c6c9e9 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1100,7 +1100,7 @@ var require_react_development = __commonJS({ var dispatcher = resolveDispatcher(); return dispatcher.useRef(initialValue); } - function useEffect5(create, deps) { + function useEffect6(create, deps) { var dispatcher = resolveDispatcher(); return dispatcher.useEffect(create, deps); } @@ -1116,7 +1116,7 @@ var require_react_development = __commonJS({ var dispatcher = resolveDispatcher(); return dispatcher.useCallback(callback2, deps); } - function useMemo2(create, deps) { + function useMemo3(create, deps) { var dispatcher = resolveDispatcher(); return dispatcher.useMemo(create, deps); } @@ -1882,12 +1882,12 @@ var require_react_development = __commonJS({ exports.useContext = useContext2; exports.useDebugValue = useDebugValue2; exports.useDeferredValue = useDeferredValue; - exports.useEffect = useEffect5; + exports.useEffect = useEffect6; exports.useId = useId; exports.useImperativeHandle = useImperativeHandle; exports.useInsertionEffect = useInsertionEffect; exports.useLayoutEffect = useLayoutEffect; - exports.useMemo = useMemo2; + exports.useMemo = useMemo3; exports.useReducer = useReducer2; exports.useRef = useRef3; exports.useState = useState; @@ -24379,10 +24379,10 @@ var require_react_jsx_runtime_development = __commonJS({ return jsxWithValidation(type, props, key, false); } } - var jsx6 = jsxWithValidationDynamic; + var jsx7 = jsxWithValidationDynamic; var jsxs4 = jsxWithValidationStatic; exports.Fragment = REACT_FRAGMENT_TYPE; - exports.jsx = jsx6; + exports.jsx = jsx7; exports.jsxs = jsxs4; })(); } @@ -24417,7 +24417,7 @@ function Header() { var Header_default = Header; // src/Picker.tsx -var import_react2 = __toESM(require_react(), 1); +var import_react3 = __toESM(require_react(), 1); // node_modules/jotai/esm/vanilla.mjs var keyCount = 0; @@ -25064,31 +25064,40 @@ function useAtom(atom2, options) { ]; } +// src/util.ts +var import_react2 = __toESM(require_react(), 1); +function useLocalAtom(initialValue, deps) { + return (0, import_react2.useMemo)(() => atom(initialValue), deps); +} +function useCommand(fn, deps) { + return useSetAtom((0, import_react2.useMemo)(() => atom(null, fn), deps)); +} + // src/Picker.tsx var import_jsx_runtime2 = __toESM(require_jsx_runtime(), 1); function Picker({ $url, - $options = (0, import_react2.useMemo)(() => atom([]), []), - $isLoading = (0, import_react2.useMemo)(() => atom(true), []), - $isEnabled = (0, import_react2.useMemo)(() => atom(true), []), - $selectedOption = (0, import_react2.useMemo)(() => atom(""), []) + $options = (0, import_react3.useMemo)(() => atom([]), []), + $isLoading = (0, import_react3.useMemo)(() => atom(true), []), + $isEnabled = (0, import_react3.useMemo)(() => atom(true), []), + $selectedOptionValue = (0, import_react3.useMemo)(() => atom(""), []) }) { const url = useAtomValue($url); const options = useAtomValue($options); const isLoading = useAtomValue($isLoading); - const [selectedOption, setSelectedOption] = useAtom($selectedOption); + const [selectedOptionValue, setSelectedOptionValue] = useAtom($selectedOptionValue); const isEnabled = useAtomValue($isEnabled); - const optionsFetched = useSetAtom((0, import_react2.useMemo)(() => atom(null, (get, set2, options2) => { + const handleFetchedOptions = useCommand((get, set2, options2) => { set2($options, options2); set2($isLoading, false); - }), [$options, $isLoading])); - (0, import_react2.useEffect)(() => { + }, [$options, $isLoading]); + (0, import_react3.useEffect)(() => { if (isEnabled) { - fetch(url).then((x) => x.json()).catch((err) => ["AAPL", "MSFT", "GOOG"]).then(optionsFetched); + fetch(url).then((x) => x.json()).catch((err) => ["AAPL", "MSFT", "GOOG"]).then(handleFetchedOptions); } }, [url, isEnabled]); - return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: isLoading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Loading..." }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("select", { value: selectedOption, onChange: (e) => { - setSelectedOption(e.target.value); + return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: isLoading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Loading..." }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("select", { value: selectedOptionValue, onChange: (e) => { + setSelectedOptionValue(e.target.value); }, children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: "" }, ""), options.map( @@ -25100,7 +25109,9 @@ function Picker({ // src/App.module.css var App_module_default = { "app": "App-module__app_gPMrEW__001", - "picker": "App-module__picker_gPMrEW__001" + "form": "App-module__form_gPMrEW__001", + "picker": "App-module__picker_gPMrEW__001", + "underlyingPrice": "App-module__underlyingPrice_gPMrEW__001" }; // node_modules/@kurkle/color/dist/color.esm.js @@ -36540,7 +36551,7 @@ __publicField(TimeSeriesScale, "id", "timeseries"); __publicField(TimeSeriesScale, "defaults", TimeScale.defaults); // node_modules/react-chartjs-2/dist/index.js -var import_react3 = __toESM(require_react(), 1); +var import_react4 = __toESM(require_react(), 1); var defaultDatasetIdKey = "label"; function reforwardRef(ref, value) { if (typeof ref === "function") { @@ -36585,8 +36596,8 @@ function cloneData(data) { } function ChartComponent(props, ref) { const { height = 150, width = 300, redraw = false, datasetIdKey, type, data, options, plugins = [], fallbackContent, updateMode, ...canvasProps } = props; - const canvasRef = (0, import_react3.useRef)(null); - const chartRef = (0, import_react3.useRef)(); + const canvasRef = (0, import_react4.useRef)(null); + const chartRef = (0, import_react4.useRef)(); const renderChart = () => { if (!canvasRef.current) return; @@ -36607,7 +36618,7 @@ function ChartComponent(props, ref) { chartRef.current = null; } }; - (0, import_react3.useEffect)(() => { + (0, import_react4.useEffect)(() => { if (!redraw && chartRef.current && options) { setOptions(chartRef.current, options); } @@ -36615,7 +36626,7 @@ function ChartComponent(props, ref) { redraw, options ]); - (0, import_react3.useEffect)(() => { + (0, import_react4.useEffect)(() => { if (!redraw && chartRef.current) { setLabels(chartRef.current.config.data, data.labels); } @@ -36623,7 +36634,7 @@ function ChartComponent(props, ref) { redraw, data.labels ]); - (0, import_react3.useEffect)(() => { + (0, import_react4.useEffect)(() => { if (!redraw && chartRef.current && data.datasets) { setDatasets(chartRef.current.config.data, data.datasets, datasetIdKey); } @@ -36631,7 +36642,7 @@ function ChartComponent(props, ref) { redraw, data.datasets ]); - (0, import_react3.useEffect)(() => { + (0, import_react4.useEffect)(() => { if (!chartRef.current) return; if (redraw) { @@ -36647,7 +36658,7 @@ function ChartComponent(props, ref) { data.datasets, updateMode ]); - (0, import_react3.useEffect)(() => { + (0, import_react4.useEffect)(() => { if (!chartRef.current) return; destroyChart(); @@ -36655,21 +36666,21 @@ function ChartComponent(props, ref) { }, [ type ]); - (0, import_react3.useEffect)(() => { + (0, import_react4.useEffect)(() => { renderChart(); return () => destroyChart(); }, []); - return /* @__PURE__ */ import_react3.default.createElement("canvas", Object.assign({ + return /* @__PURE__ */ import_react4.default.createElement("canvas", Object.assign({ ref: canvasRef, role: "img", height, width }, canvasProps), fallbackContent); } -var Chart2 = /* @__PURE__ */ (0, import_react3.forwardRef)(ChartComponent); +var Chart2 = /* @__PURE__ */ (0, import_react4.forwardRef)(ChartComponent); function createTypedChart(type, registerables) { Chart.register(registerables); - return /* @__PURE__ */ (0, import_react3.forwardRef)((props, ref) => /* @__PURE__ */ import_react3.default.createElement(Chart2, Object.assign({}, props, { + return /* @__PURE__ */ (0, import_react4.forwardRef)((props, ref) => /* @__PURE__ */ import_react4.default.createElement(Chart2, Object.assign({}, props, { ref, type }))); @@ -36784,7 +36795,7 @@ function loadable(anAtom) { } // src/CalendarPricesChart.tsx -var import_react4 = __toESM(require_react(), 1); +var import_react5 = __toESM(require_react(), 1); var import_jsx_runtime3 = __toESM(require_jsx_runtime(), 1); Chart.register(LineElement, plugin_tooltip, plugin_legend, CategoryScale, LinearScale, PointElement); var $prices = loadable(atom(async (get) => { @@ -36800,7 +36811,7 @@ var $prices = loadable(atom(async (get) => { })); function CalendarPricesChart() { const prices = useAtomValue($prices); - (0, import_react4.useEffect)(() => { + (0, import_react5.useEffect)(() => { console.log(prices); }, [prices]); if (prices.state === "hasData") { @@ -36826,8 +36837,25 @@ function CalendarPricesChart() { } } -// src/App.tsx +// src/UnderlyingPrice.tsx +var import_react6 = __toESM(require_react(), 1); var import_jsx_runtime4 = __toESM(require_jsx_runtime(), 1); +function UnderlyingPrice({ $underlying, $quoteDate }) { + const underlying = useAtomValue($underlying); + const quoteDate = useAtomValue($quoteDate); + const $underlyingPrice = useLocalAtom("", [$underlying, $quoteDate]); + const [underlyingPrice, setUnderlyingPrice] = useAtom($underlyingPrice); + const handleInit = useCommand(() => { + fetch(`${baseUrl}/underlying_quotes/${underlying}/${quoteDate.substring(0, 10)}`).then((x) => x.json()).then((rows) => { + setUnderlyingPrice(rows[0].close.toString()); + }); + }, [underlying, quoteDate]); + (0, import_react6.useEffect)(handleInit, [underlying, quoteDate]); + return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: underlyingPrice }); +} + +// src/App.tsx +var import_jsx_runtime5 = __toESM(require_jsx_runtime(), 1); var baseUrl = "http://127.0.0.1:8234"; var $underlyingsUrl = atom(`${baseUrl}/option_quotes/underlyings`); var $selectedUnderlying = atom(""); @@ -36844,38 +36872,46 @@ var $backMonthExpirationPickerUrl = atom((get) => `${baseUrl}/option_quotes/${ge var $isBackMonthExpirationPickerEnabled = atom((get) => get($selectedQuoteDate) !== "" && get($selectedUnderlying) !== ""); var $selectedBackExpiration = atom(""); function App() { - return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: App_module_default.app, children: [ - /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Header_default, {}), - /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: App_module_default.picker, children: [ - /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { children: "Underlying" }), - /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Picker, { $url: $underlyingsUrl, $selectedOption: $selectedUnderlying }) - ] }), - /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: App_module_default.picker, children: [ - /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { children: "Quote Date" }), - /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Picker, { $url: $quoteDatePickerUrl, $isEnabled: $isQuoteDatePickerEnabled, $selectedOption: $selectedQuoteDate }) + const selectedUnderlying = useAtomValue($selectedUnderlying); + return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: App_module_default.app, children: [ + /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Header_default, {}), + /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: App_module_default.form, children: [ + /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: App_module_default.underlyingPrice, children: [ + /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: "Underlying: " }), + /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: selectedUnderlying }), + /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(UnderlyingPrice, { $underlying: $selectedUnderlying, $quoteDate: $selectedQuoteDate }) + ] }), + /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: App_module_default.picker, children: [ + /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { children: "Underlying" }), + /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Picker, { $url: $underlyingsUrl, $selectedOptionValue: $selectedUnderlying }) + ] }), + /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: App_module_default.picker, children: [ + /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { children: "Quote Date" }), + /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Picker, { $url: $quoteDatePickerUrl, $isEnabled: $isQuoteDatePickerEnabled, $selectedOptionValue: $selectedQuoteDate }) + ] }), + /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: App_module_default.picker, children: [ + /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { children: "Strike" }), + /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Picker, { $url: $strikePickerUrl, $isEnabled: $isStrikePickerEnabled, $selectedOptionValue: $selectedStrike }) + ] }), + /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: App_module_default.picker, children: [ + /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { children: "Front Expiration" }), + /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Picker, { $url: $frontMonthExpirationPickerUrl, $isEnabled: $isFrontMonthExpirationPickerEnabled, $selectedOptionValue: $selectedFrontExpiration }) + ] }), + /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: App_module_default.picker, children: [ + /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { children: "Back Expiration" }), + /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Picker, { $url: $backMonthExpirationPickerUrl, $isEnabled: $isBackMonthExpirationPickerEnabled, $selectedOptionValue: $selectedBackExpiration }) + ] }) ] }), - /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: App_module_default.picker, children: [ - /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { children: "Strike" }), - /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Picker, { $url: $strikePickerUrl, $isEnabled: $isStrikePickerEnabled, $selectedOption: $selectedStrike }) - ] }), - /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: App_module_default.picker, children: [ - /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { children: "Front Expiration" }), - /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Picker, { $url: $frontMonthExpirationPickerUrl, $isEnabled: $isFrontMonthExpirationPickerEnabled, $selectedOption: $selectedFrontExpiration }) - ] }), - /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: App_module_default.picker, children: [ - /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { children: "Back Expiration" }), - /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Picker, { $url: $backMonthExpirationPickerUrl, $isEnabled: $isBackMonthExpirationPickerEnabled, $selectedOption: $selectedBackExpiration }) - ] }), - /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CalendarPricesChart, {}) + /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CalendarPricesChart, {}) ] }); } var App_default = App; // src/index.tsx -var import_jsx_runtime5 = __toESM(require_jsx_runtime(), 1); +var import_jsx_runtime6 = __toESM(require_jsx_runtime(), 1); var rootEl = document.getElementById("app"); var Root = (0, import_client.createRoot)(rootEl); -Root.render(/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(App_default, {})); +Root.render(/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(App_default, {})); /*! Bundled license information: react/cjs/react.development.js: diff --git a/src/App.module.css b/src/App.module.css index 828a383..9ba9ad5 100644 --- a/src/App.module.css +++ b/src/App.module.css @@ -2,7 +2,18 @@ display:flex; flex-direction: column; } -.app > .picker { +.app > .form { + display:flex; + flex-direction: column; + max-width: 30em; +} +.app > .form > .underlyingPrice { + display: flex; + flex-direction: row; + justify-content: flex-start; + gap: 0.6em; +} +.app > .form > .picker { display: flex; flex-direction: row; justify-content: space-between; diff --git a/src/App.tsx b/src/App.tsx index a6f7325..568d627 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,12 +2,11 @@ import Header from './Header'; import { HistoricalImpliedVolatilityChart } from "./HistoricalImpliedVolatilityChart"; import { Picker } from './Picker'; import { atom as $, useAtomValue } from 'jotai'; -import { loadable } from 'jotai/utils'; //import './index.css'; //@ts-ignore import k from './App.module.css'; -import { useEffect } from 'react'; import { CalendarPricesChart } from './CalendarPricesChart'; +import { UnderlyingPrice } from './UnderlyingPrice'; export const baseUrl = 'http://127.0.0.1:8234'; @@ -32,16 +31,19 @@ export const $isBackMonthExpirationPickerEnabled = $((get) => get($selectedQuote export const $selectedBackExpiration = $(''); - function App() { + const selectedUnderlying = useAtomValue($selectedUnderlying); return (
-
-
-
-
-
+
+
Underlying: {selectedUnderlying}
+
+
+
+
+
+
{/* */}
diff --git a/src/Picker.tsx b/src/Picker.tsx index c3a7a71..20f620e 100644 --- a/src/Picker.tsx +++ b/src/Picker.tsx @@ -1,10 +1,12 @@ import { useEffect, useMemo } from "react"; import { atom as $, useAtom, Atom, PrimitiveAtom, useAtomValue, useSetAtom, } from 'jotai'; +import { baseUrl } from "./App"; +import { useCommand } from "./util"; type PickerInput = { $options?:PrimitiveAtom>, $isLoading?:PrimitiveAtom, - $selectedOption?:PrimitiveAtom, + $selectedOptionValue?:PrimitiveAtom, $url:Atom, $isEnabled?:Atom }; @@ -13,25 +15,25 @@ export function Picker({ $options = useMemo(()=>$([]), []), $isLoading = useMemo(()=>$(true),[]), $isEnabled = useMemo(()=>$(true),[]), - $selectedOption = useMemo(()=>$(''), []) + $selectedOptionValue = useMemo(()=>$(''), []) }: PickerInput){ const url = useAtomValue($url); const options = useAtomValue($options); const isLoading = useAtomValue($isLoading); - const [selectedOption, setSelectedOption] = useAtom($selectedOption); + const [selectedOptionValue, setSelectedOptionValue] = useAtom($selectedOptionValue); const isEnabled = useAtomValue($isEnabled); - const optionsFetched = useSetAtom(useMemo(()=>$(null, (get,set,options)=>{ + const handleFetchedOptions = useCommand((get,set,options)=>{ set($options, options); set($isLoading, false); - }),[$options, $isLoading])); + }, [$options, $isLoading]); useEffect(()=>{ if(isEnabled){ fetch(url) .then(x=>x.json()) .catch((err)=>['AAPL', 'MSFT', 'GOOG']) - .then(optionsFetched) + .then(handleFetchedOptions) } },[url, isEnabled]) @@ -41,7 +43,7 @@ export function Picker({ ? Loading... : - { setSelectedOptionValue(e.target.value); }}> {options.map((date)=> diff --git a/src/UnderlyingPrice.tsx b/src/UnderlyingPrice.tsx new file mode 100644 index 0000000..775f654 --- /dev/null +++ b/src/UnderlyingPrice.tsx @@ -0,0 +1,23 @@ +import { atom as $, useAtom, useAtomValue } from 'jotai'; +import { useEffect } from 'react'; +import { useCommand, useLocalAtom } from './util'; +import { baseUrl } from './App'; + +export function UnderlyingPrice({$underlying, $quoteDate}){ + const underlying = useAtomValue($underlying); + const quoteDate = useAtomValue($quoteDate) as String; + const $underlyingPrice = useLocalAtom('', [$underlying, $quoteDate]); + const [underlyingPrice, setUnderlyingPrice] = useAtom($underlyingPrice); + + const handleInit = useCommand(()=>{ + fetch(`${baseUrl}/underlying_quotes/${underlying}/${quoteDate.substring(0,10)}`).then((x)=>x.json()) + .then((rows)=>{ setUnderlyingPrice(rows[0].close.toString()); }); + },[underlying, quoteDate]); + + //@ts-ignore + useEffect(handleInit,[underlying, quoteDate]); + + return ( + {underlyingPrice} + ); +} \ No newline at end of file diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..f96d436 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,96 @@ +import { useSetAtom, atom as $ } from "jotai"; +import { useMemo } from "react"; + +// from [https://stackoverflow.com/a/14873282] +function erf(x) { + // save the sign of x + var sign = (x >= 0) ? 1 : -1; + x = Math.abs(x); + + // constants + var a1 = 0.254829592; + var a2 = -0.284496736; + var a3 = 1.421413741; + var a4 = -1.453152027; + var a5 = 1.061405429; + var p = 0.3275911; + + // A&S formula 7.1.26 + var t = 1.0/(1.0 + p*x); + var y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x); + return sign * y; // erf(-x) = -erf(x); +} + +// produced by ChatGPT +export function calculateImpliedVolatility(optionPrice, underlyingPrice, strikePrice, timeToExpiration, riskFreeRate, optionType, maxIterations = 100, tolerance = 0.0001) { + let iv = 0.5; // Initial guess for implied volatility + let epsilon = 1e-6; // Small value to avoid division by zero + + for (let i = 0; i < maxIterations; i++) { + const optionPriceEstimate = calculateOptionPrice(underlyingPrice, strikePrice, timeToExpiration, iv, riskFreeRate, optionType); + const vega = calculateVega(underlyingPrice, strikePrice, timeToExpiration, iv, riskFreeRate); + const diff = optionPrice - optionPriceEstimate; + + if (Math.abs(diff) < tolerance) { + return iv; + } + + iv = iv + (diff / (vega || epsilon)); // Avoid division by zero + } + + return NaN; // If max iterations are reached, return NaN (no convergence) +} + +function calculateOptionPrice(S, K, T, impliedVolatility, r, optionType) { + const d1 = (Math.log(S / K) + (r + (impliedVolatility ** 2) / 2) * T) / (impliedVolatility * Math.sqrt(T)); + const d2 = d1 - impliedVolatility * Math.sqrt(T); + + if (optionType === 'call') { + return S * Math.exp(-r * T) * cumulativeDistributionFunction(d1) - K * Math.exp(-r * T) * cumulativeDistributionFunction(d2); + } else if (optionType === 'put') { + return K * Math.exp(-r * T) * cumulativeDistributionFunction(-d2) - S * Math.exp(-r * T) * cumulativeDistributionFunction(-d1); + } else { + throw new Error('Invalid option type. Use "call" or "put".'); + } +} + +function calculateVega(S, K, T, impliedVolatility, r) { + const d1 = (Math.log(S / K) + (r + (impliedVolatility ** 2) / 2) * T) / (impliedVolatility * Math.sqrt(T)); + return S * Math.sqrt(T) * probabilityDensityFunction(d1); +} + +function cumulativeDistributionFunction(x) { + return 0.5 * (1 + erf(x / Math.sqrt(2))); +} + +function probabilityDensityFunction(x) { + return Math.exp(-0.5 * x ** 2) / Math.sqrt(2 * Math.PI); +} + +// Example usage +/* +const optionPrice = 5.25; // Example option price +const underlyingPrice = 50; // Example underlying stock price +const strikePrice = 50; // Example strike price +const timeToExpiration = 0.25; // Example time to expiration (in years) +const riskFreeRate = 0.03; // Example risk-free interest rate +const optionType = 'call'; // Example option type ('call' or 'put') + +const impliedVolatility = calculateImpliedVolatility(optionPrice, underlyingPrice, strikePrice, timeToExpiration, riskFreeRate, optionType); +console.log('Implied Volatility:', impliedVolatility); +*/ + +export function useLocalAtom(initialValue, deps){ + return useMemo(()=>$(initialValue), deps); +} + +/** + * Define a "command": a function that mutates state. It's passed `get` and `set` functions to access Jotai atoms, in addition to any other parameters. + * The function is memoized and is returned in the form of a Jotai "set" atom. It's called like any other function. + * @param fn The function to memoize + * @param deps Dependency array; when to re-memoize the function + * @returns + */ +export function useCommand(fn, deps){ + return useSetAtom(useMemo(()=>$(null, fn),deps)); +} \ No newline at end of file