Compare commits
13 Commits
incrementa
...
master
Author | SHA1 | Date |
---|---|---|
|
74f1358386 | 2 years ago |
|
9dec57681f | 2 years ago |
|
582d868568 | 2 years ago |
|
f3698ec8bd | 2 years ago |
|
0856c9f3d4 | 2 years ago |
|
f54b42d0ad | 2 years ago |
|
2fb6e8506b | 2 years ago |
|
60921dcc76 | 2 years ago |
|
bebd996796 | 2 years ago |
|
d49caf4bde | 2 years ago |
|
87f5d5f6d5 | 2 years ago |
|
345bded212 | 2 years ago |
|
b02c71d206 | 2 years ago |
@ -0,0 +1,9 @@
|
|||||||
|
Choose Underlying
|
||||||
|
Choose Quote Date (i.e. from which point in time are you running the analysis)
|
||||||
|
Choose Strike
|
||||||
|
For each front-month-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, for all possible underlying prices.
|
||||||
|
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.
|
@ -0,0 +1,21 @@
|
|||||||
|
/* esbuild-css-modules-plugin-ns-css:src/App.module.css */
|
||||||
|
.App-module__app_gPMrEW__001 {
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
// src/App.module.css
|
||||||
|
var App_module_default = {
|
||||||
|
"app": "App-module__app_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
|
||||||
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
/* esbuild-css-modules-plugin-ns-css:src/App.module.css */
|
||||||
|
.App-module__app_gPMrEW__001 {
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,20 @@
|
|||||||
|
.app {
|
||||||
|
display:flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
import { Chart as ChartJS, Tooltip, Legend, LineElement, CategoryScale, LinearScale, PointElement } from "chart.js";
|
||||||
|
import { Line } from 'react-chartjs-2';
|
||||||
|
import { atom as $, useAtomValue } from 'jotai';
|
||||||
|
import { loadable } from 'jotai/utils';
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { baseUrl, $selectedUnderlying, $selectedStrike, $selectedFrontExpiration, $selectedBackExpiration } from "./App";
|
||||||
|
|
||||||
|
ChartJS.register(LineElement, Tooltip, Legend, CategoryScale, LinearScale, PointElement);
|
||||||
|
|
||||||
|
export type CalendarPrice = {
|
||||||
|
quote_date: string,
|
||||||
|
price: number
|
||||||
|
};
|
||||||
|
|
||||||
|
const $prices = loadable<Promise<Array<CalendarPrice>>>($(async (get)=>{
|
||||||
|
const selectedUnderlying = get($selectedUnderlying);
|
||||||
|
const selectedStrike = get($selectedStrike);
|
||||||
|
const selectedFrontExpiration = get($selectedFrontExpiration);
|
||||||
|
const selectedBackExpiration = get($selectedBackExpiration);
|
||||||
|
if(selectedUnderlying!=='' && selectedStrike!=='' && selectedFrontExpiration!=='' && selectedBackExpiration!==''){
|
||||||
|
return await fetch(`${baseUrl}/option_quotes/${selectedUnderlying}/${selectedStrike}/${selectedFrontExpiration}/${selectedBackExpiration}`).then(x=>x.json()).catch(()=>[]);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function CalendarPricesChart(){
|
||||||
|
const prices = useAtomValue($prices);
|
||||||
|
|
||||||
|
useEffect(()=>{ console.log(prices); },[prices])
|
||||||
|
if(prices.state === 'hasData'){
|
||||||
|
return (<div>
|
||||||
|
<Line
|
||||||
|
datasetIdKey='id'
|
||||||
|
data={{
|
||||||
|
labels: prices.data.map((x)=>x.quote_date.substring(0,10)),
|
||||||
|
datasets: [
|
||||||
|
{//@ts-ignore
|
||||||
|
id: 1,
|
||||||
|
label: '',
|
||||||
|
data: prices.data.map((x)=>x.price),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return (<></>);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import { Chart as ChartJS, Tooltip, Legend, LineElement, CategoryScale, LinearScale, PointElement } from "chart.js";
|
||||||
|
import { Line } from 'react-chartjs-2';
|
||||||
|
|
||||||
|
ChartJS.register(LineElement, Tooltip, Legend, CategoryScale, LinearScale, PointElement);
|
||||||
|
|
||||||
|
export function HistoricalImpliedVolatilityChart(){
|
||||||
|
return (<div>
|
||||||
|
<Line
|
||||||
|
datasetIdKey='id'
|
||||||
|
data={{
|
||||||
|
labels: ['Jun', 'Jul', 'Aug'],
|
||||||
|
datasets: [
|
||||||
|
{//@ts-ignore
|
||||||
|
id: 1,
|
||||||
|
label: '',
|
||||||
|
data: [5, 6, 7],
|
||||||
|
},
|
||||||
|
{//@ts-ignore
|
||||||
|
id: 2,
|
||||||
|
label: '',
|
||||||
|
data: [3, 2, 1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>);
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
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<Array<string>>,
|
||||||
|
$isLoading?:PrimitiveAtom<boolean>,
|
||||||
|
$selectedOptionValue?:PrimitiveAtom<string>,
|
||||||
|
$url:Atom<string>,
|
||||||
|
$isEnabled?:Atom<boolean>
|
||||||
|
};
|
||||||
|
export function Picker({
|
||||||
|
$url,
|
||||||
|
$options = useMemo(()=>$([]), []),
|
||||||
|
$isLoading = useMemo(()=>$(true),[]),
|
||||||
|
$isEnabled = useMemo(()=>$(true),[]),
|
||||||
|
$selectedOptionValue = useMemo(()=>$(''), [])
|
||||||
|
}: PickerInput){
|
||||||
|
const url = useAtomValue($url);
|
||||||
|
const options = useAtomValue($options);
|
||||||
|
const isLoading = useAtomValue($isLoading);
|
||||||
|
const [selectedOptionValue, setSelectedOptionValue] = useAtom($selectedOptionValue);
|
||||||
|
const isEnabled = useAtomValue($isEnabled);
|
||||||
|
|
||||||
|
const handleFetchedOptions = useCommand((get,set,options)=>{
|
||||||
|
set($options, options);
|
||||||
|
set($isLoading, false);
|
||||||
|
}, [$options, $isLoading]);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
if(isEnabled){
|
||||||
|
fetch(url)
|
||||||
|
.then(x=>x.json())
|
||||||
|
.catch((err)=>['AAPL', 'MSFT', 'GOOG'])
|
||||||
|
.then(handleFetchedOptions)
|
||||||
|
}
|
||||||
|
},[url, isEnabled])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isLoading
|
||||||
|
?
|
||||||
|
<span>Loading...</span>
|
||||||
|
:
|
||||||
|
<select value={selectedOptionValue} onChange={(e)=>{ setSelectedOptionValue(e.target.value); }}>
|
||||||
|
<option key="" value=""></option>
|
||||||
|
{options.map((date)=>
|
||||||
|
<option key={date} value={date}>{date}</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,32 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { atom as $, useAtom } from 'jotai';
|
|
||||||
|
|
||||||
const $dates = $<Array<string>>([]);
|
|
||||||
const $isLoading = $<boolean>(false);
|
|
||||||
|
|
||||||
export function QuoteDatePicker(){
|
|
||||||
const [dates, setDates] = useAtom($dates);
|
|
||||||
const [isLoading, setIsLoading] = useAtom($isLoading);
|
|
||||||
|
|
||||||
useEffect(()=>{
|
|
||||||
fetch('http://127.0.0.1:8234/option_quotes/AAPL/quote_dates')
|
|
||||||
.then(x=>x.json())
|
|
||||||
.catch((err)=>['2021-01-02', '2021-01-03', '2021-01-04'])
|
|
||||||
.then((dates_)=>{ setDates(dates_); setIsLoading(false); })
|
|
||||||
},[])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{isLoading
|
|
||||||
?
|
|
||||||
<span>Loading...</span>
|
|
||||||
:
|
|
||||||
<select>
|
|
||||||
{dates.map((date)=>
|
|
||||||
<option key={date} value={date}>{date}</option>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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 (
|
||||||
|
<span>{underlyingPrice}</span>
|
||||||
|
);
|
||||||
|
}
|
@ -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=0.03, optionType='call', 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));
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2017"]
|
||||||
},
|
},
|
||||||
}
|
}
|
Loading…
Reference in New Issue