bugfixing options calculator
This commit is contained in:
parent
bfed732d72
commit
f027ebe0da
@ -64,7 +64,7 @@
|
|||||||
let optionSymbol: string;
|
let optionSymbol: string;
|
||||||
let breakEvenPrice: number | null = null;
|
let breakEvenPrice: number | null = null;
|
||||||
let totalPremium: number;
|
let totalPremium: number;
|
||||||
let limits: Record<string, string> = {};
|
let metrics: Record<string, string> = {};
|
||||||
let rawData: Record<string, any> = {};
|
let rawData: Record<string, any> = {};
|
||||||
|
|
||||||
// Search variables
|
// Search variables
|
||||||
@ -102,7 +102,7 @@
|
|||||||
// Other strategies commented out in original code
|
// Other strategies commented out in original code
|
||||||
];
|
];
|
||||||
|
|
||||||
let userStrategy: OptionLeg[] = [];
|
let userStrategy = [];
|
||||||
let description = prebuiltStrategy[0]?.description;
|
let description = prebuiltStrategy[0]?.description;
|
||||||
|
|
||||||
// STRATEGY FUNCTIONS
|
// STRATEGY FUNCTIONS
|
||||||
@ -134,33 +134,49 @@
|
|||||||
selectedOptionType = null;
|
selectedOptionType = null;
|
||||||
selectedAction = null;
|
selectedAction = null;
|
||||||
}
|
}
|
||||||
|
userStrategy = [
|
||||||
await loadData("default");
|
{
|
||||||
|
strike: selectedStrike,
|
||||||
|
optionType: selectedOptionType,
|
||||||
|
date: selectedDate,
|
||||||
|
optionPrice: selectedOptionPrice,
|
||||||
|
quantity: selectedQuantity,
|
||||||
|
action: selectedAction,
|
||||||
|
},
|
||||||
|
];
|
||||||
shouldUpdate = true;
|
shouldUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PAYOFF CALCULATION FUNCTIONS
|
// PAYOFF CALCULATION FUNCTIONS
|
||||||
|
|
||||||
const payoffFunctions = {
|
const payoffFunctions = {
|
||||||
"Buy Call": (s: number, strike: number, premium: number) =>
|
"Buy Call": (
|
||||||
s < strike ? -premium : (s - strike) * 100 * selectedQuantity - premium,
|
s: number,
|
||||||
|
strike: number,
|
||||||
|
premium: number,
|
||||||
|
quantity: number,
|
||||||
|
) => (s < strike ? -premium : (s - strike) * 100 * quantity - premium),
|
||||||
|
|
||||||
"Sell Call": (s: number, strike: number, premium: number) =>
|
"Sell Call": (
|
||||||
s < strike ? premium : premium - (s - strike) * 100 * selectedQuantity,
|
s: number,
|
||||||
|
strike: number,
|
||||||
|
premium: number,
|
||||||
|
quantity: number,
|
||||||
|
) => (s < strike ? premium : premium - (s - strike) * 100 * quantity),
|
||||||
|
|
||||||
"Buy Put": (s: number, strike: number, premium: number) =>
|
"Buy Put": (
|
||||||
s > strike ? -premium : (strike - s) * 100 * selectedQuantity - premium,
|
s: number,
|
||||||
|
strike: number,
|
||||||
|
premium: number,
|
||||||
|
quantity: number,
|
||||||
|
) => (s > strike ? -premium : (strike - s) * 100 * quantity - premium),
|
||||||
|
|
||||||
"Sell Put": (s: number, strike: number, premium: number) =>
|
"Sell Put": (
|
||||||
s > strike ? premium : premium - (strike - s) * 100 * selectedQuantity,
|
s: number,
|
||||||
};
|
strike: number,
|
||||||
|
premium: number,
|
||||||
// Define break-even calculators for each scenario (using per-share price)
|
quantity: number,
|
||||||
const breakEvenCalculators = {
|
) => (s > strike ? premium : premium - (strike - s) * 100 * quantity),
|
||||||
"Buy Call": (strike: number, optionPrice: number) => strike + optionPrice,
|
|
||||||
"Sell Call": (strike: number, optionPrice: number) => strike + optionPrice,
|
|
||||||
"Buy Put": (strike: number, optionPrice: number) => strike - optionPrice,
|
|
||||||
"Sell Put": (strike: number, optionPrice: number) => strike - optionPrice,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
@ -200,6 +216,7 @@
|
|||||||
s,
|
s,
|
||||||
leg.strike,
|
leg.strike,
|
||||||
legPremium,
|
legPremium,
|
||||||
|
leg.quantity,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error(
|
||||||
@ -211,9 +228,9 @@
|
|||||||
dataPoints.push([s, aggregatedPayoff]);
|
dataPoints.push([s, aggregatedPayoff]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate break-even and limits for single-leg strategies
|
// Calculate break-even and metrics for single-leg strategies
|
||||||
calculateBreakEvenAndLimits();
|
calculateMetrics();
|
||||||
|
calculateBreakevenPrice(dataPoints);
|
||||||
// Build the chart options
|
// Build the chart options
|
||||||
const options = {
|
const options = {
|
||||||
credits: { enabled: false },
|
credits: { enabled: false },
|
||||||
@ -258,9 +275,11 @@
|
|||||||
dashStyle: "Dash",
|
dashStyle: "Dash",
|
||||||
width: $screenWidth < 640 ? 0 : 1.5,
|
width: $screenWidth < 640 ? 0 : 1.5,
|
||||||
label: {
|
label: {
|
||||||
text: `<span class="hidden sm:block text-black dark:text-white text-sm">Breakeven $${breakEvenPrice.toFixed(
|
text: `<span class="hidden sm:block text-black dark:text-white text-sm">Breakeven $${
|
||||||
2,
|
typeof breakEvenPrice === "number"
|
||||||
)}</span>`,
|
? breakEvenPrice.toFixed(2)
|
||||||
|
: ""
|
||||||
|
}</span>`,
|
||||||
style: { color: $mode === "light" ? "black" : "white" },
|
style: { color: $mode === "light" ? "black" : "white" },
|
||||||
},
|
},
|
||||||
zIndex: 5,
|
zIndex: 5,
|
||||||
@ -346,49 +365,55 @@
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateBreakEvenAndLimits() {
|
function calculateMetrics() {
|
||||||
|
const multiplier = 100;
|
||||||
|
|
||||||
let totalPremium = 0;
|
let totalPremium = 0;
|
||||||
let overallMaxProfit = 0;
|
let overallMaxProfit = 0;
|
||||||
let overallMaxLoss = 0;
|
let overallMaxLoss = 0;
|
||||||
let unlimitedProfit = false;
|
let unlimitedProfit = false;
|
||||||
let unlimitedLoss = false;
|
let unlimitedLoss = false;
|
||||||
|
|
||||||
// Loop through each leg in the strategy
|
// Loop through each leg in the strategy.
|
||||||
for (let i = 0; i < userStrategy.length; i++) {
|
for (let i = 0; i < userStrategy.length; i++) {
|
||||||
const leg = userStrategy[i];
|
const leg = userStrategy[i];
|
||||||
totalPremium += leg.optionPrice; // accumulate total premium
|
const quantity = leg?.quantity || 1; // Default to 1 if quantity is missing.
|
||||||
const scenarioKey = `${leg?.action} ${leg?.optionType}`;
|
|
||||||
|
|
||||||
|
// Multiply the premium by the contract multiplier and quantity.
|
||||||
|
totalPremium += leg.optionPrice * multiplier * quantity;
|
||||||
|
const scenarioKey = `${leg?.action} ${leg?.optionType}`;
|
||||||
let legProfit = 0;
|
let legProfit = 0;
|
||||||
let legLoss = 0;
|
let legLoss = 0;
|
||||||
|
|
||||||
// Determine limits for each leg based on its scenario
|
// Determine metrics for each leg based on its scenario.
|
||||||
if (scenarioKey === "Buy Call") {
|
if (scenarioKey === "Buy Call") {
|
||||||
// Profit is unlimited for a long call, loss limited to the premium paid.
|
// Long call: unlimited profit, limited loss (the premium paid).
|
||||||
legProfit = Infinity;
|
legProfit = Infinity;
|
||||||
legLoss = -leg.optionPrice;
|
legLoss = leg.optionPrice * multiplier * quantity;
|
||||||
unlimitedProfit = true;
|
unlimitedProfit = true;
|
||||||
} else if (scenarioKey === "Sell Call") {
|
} else if (scenarioKey === "Sell Call") {
|
||||||
// Profit is limited to the premium received; loss is unlimited.
|
// Short call: limited profit (premium received), unlimited loss.
|
||||||
legProfit = leg.optionPrice;
|
legProfit = leg.optionPrice * multiplier * quantity;
|
||||||
legLoss = -Infinity;
|
legLoss = -Infinity;
|
||||||
unlimitedLoss = true;
|
unlimitedLoss = true;
|
||||||
} else if (scenarioKey === "Buy Put") {
|
} else if (scenarioKey === "Buy Put") {
|
||||||
// For a long put, profit is (strike * 100) minus the premium, loss is the premium paid.
|
// Long put: profit is (strike * multiplier minus premium) and loss is the premium paid.
|
||||||
legProfit = leg.strike * 100 - leg.optionPrice;
|
legProfit =
|
||||||
legLoss = -leg.optionPrice;
|
(leg.strike * multiplier - leg.optionPrice * multiplier) * quantity;
|
||||||
|
legLoss = leg.optionPrice * multiplier * quantity;
|
||||||
} else if (scenarioKey === "Sell Put") {
|
} else if (scenarioKey === "Sell Put") {
|
||||||
// For a short put, profit is the premium received, loss is (strike * 100 - premium).
|
// Short put: profit is the premium received;
|
||||||
legProfit = leg.optionPrice;
|
// Maximum loss is the difference between strike * multiplier and the premium, scaled by quantity.
|
||||||
legLoss = -(leg.strike * 100 - leg.optionPrice);
|
legProfit = leg.optionPrice * multiplier * quantity;
|
||||||
|
legLoss =
|
||||||
|
(leg.strike * multiplier - leg.optionPrice * multiplier) * quantity;
|
||||||
} else {
|
} else {
|
||||||
console.error("Limits not defined for scenario:", scenarioKey);
|
console.error("Metrics not defined for scenario:", scenarioKey);
|
||||||
// Defaulting to zero contribution if unknown
|
|
||||||
legProfit = 0;
|
legProfit = 0;
|
||||||
legLoss = 0;
|
legLoss = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sum only the finite numbers. If any leg is unlimited, the corresponding flag is set.
|
// Sum only the finite numbers.
|
||||||
if (isFinite(legProfit)) {
|
if (isFinite(legProfit)) {
|
||||||
overallMaxProfit += legProfit;
|
overallMaxProfit += legProfit;
|
||||||
}
|
}
|
||||||
@ -397,8 +422,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format the aggregated limits: if any leg was unlimited, set overall accordingly.
|
// Format the aggregated metrics.
|
||||||
limits = {
|
metrics = {
|
||||||
maxProfit: unlimitedProfit
|
maxProfit: unlimitedProfit
|
||||||
? "Unlimited"
|
? "Unlimited"
|
||||||
: `$${formatCurrency(overallMaxProfit)}`,
|
: `$${formatCurrency(overallMaxProfit)}`,
|
||||||
@ -406,30 +431,38 @@
|
|||||||
? "Unlimited"
|
? "Unlimited"
|
||||||
: `$${formatCurrency(overallMaxLoss)}`,
|
: `$${formatCurrency(overallMaxLoss)}`,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// For break-even price, if there's exactly one leg, compute it; otherwise, set it to null.
|
function calculateBreakevenPrice(dataPoints) {
|
||||||
if (userStrategy.length === 1) {
|
breakEvenPrice = null;
|
||||||
const leg = userStrategy[0];
|
// Loop over the dataPoints to find a sign change from loss to profit or vice versa
|
||||||
const scenarioKey = `${leg?.action} ${leg?.optionType}`;
|
for (let i = 1; i < dataPoints.length; i++) {
|
||||||
if (breakEvenCalculators[scenarioKey]) {
|
const [prevPrice, prevProfitLoss] = dataPoints[i - 1];
|
||||||
breakEvenPrice = breakEvenCalculators[scenarioKey](
|
const [currPrice, currProfitLoss] = dataPoints[i];
|
||||||
leg.strike,
|
|
||||||
leg.optionPrice,
|
// Check if there is a sign change between consecutive points
|
||||||
);
|
if (
|
||||||
|
(prevProfitLoss < 0 && currProfitLoss >= 0) ||
|
||||||
|
(prevProfitLoss > 0 && currProfitLoss <= 0)
|
||||||
|
) {
|
||||||
|
// Linear interpolation to estimate the exact crossing point
|
||||||
|
const priceDiff = currPrice - prevPrice;
|
||||||
|
const profitDiff = currProfitLoss - prevProfitLoss;
|
||||||
|
const ratio = Math.abs(prevProfitLoss) / Math.abs(profitDiff);
|
||||||
|
breakEvenPrice = prevPrice + ratio * priceDiff;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function for currency formatting
|
// Helper function for currency formatting
|
||||||
function formatCurrency(value: number): string {
|
function formatCurrency(value: number): string {
|
||||||
return value.toLocaleString("en-US", {
|
return value?.toLocaleString("en-US", {
|
||||||
minimumFractionDigits: 2,
|
minimumFractionDigits: 2,
|
||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// DATA LOADING FUNCTIONS
|
|
||||||
|
|
||||||
const getContractHistory = async (contractId: string) => {
|
const getContractHistory = async (contractId: string) => {
|
||||||
const cacheKey = contractId;
|
const cacheKey = contractId;
|
||||||
const cachedData = getCache(cacheKey, "getContractHistory");
|
const cachedData = getCache(cacheKey, "getContractHistory");
|
||||||
@ -473,8 +506,6 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoaded = false;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (userStrategy?.length > 0) {
|
if (userStrategy?.length > 0) {
|
||||||
for (const item of userStrategy) {
|
for (const item of userStrategy) {
|
||||||
@ -490,7 +521,7 @@
|
|||||||
item.date = selectedDate;
|
item.date = selectedDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
strikeList = optionData[selectedDate] || [];
|
strikeList = optionData[item.date] || [];
|
||||||
item.strikeList = strikeList;
|
item.strikeList = strikeList;
|
||||||
|
|
||||||
// Find closest strike to current stock price
|
// Find closest strike to current stock price
|
||||||
@ -508,9 +539,9 @@
|
|||||||
// Get option price
|
// Get option price
|
||||||
optionSymbol = buildOptionSymbol(
|
optionSymbol = buildOptionSymbol(
|
||||||
selectedTicker,
|
selectedTicker,
|
||||||
selectedDate,
|
item?.date,
|
||||||
selectedOptionType,
|
item?.optionType,
|
||||||
selectedStrike,
|
item?.strike,
|
||||||
);
|
);
|
||||||
|
|
||||||
const output = await getContractHistory(optionSymbol);
|
const output = await getContractHistory(optionSymbol);
|
||||||
@ -591,27 +622,6 @@
|
|||||||
|
|
||||||
rawData = (await response.json()) || {};
|
rawData = (await response.json()) || {};
|
||||||
currentStockPrice = rawData?.getStockQuote?.price || 0;
|
currentStockPrice = rawData?.getStockQuote?.price || 0;
|
||||||
|
|
||||||
// Initialize option data
|
|
||||||
if (rawData?.getData) {
|
|
||||||
optionData = rawData.getData[selectedOptionType] || {};
|
|
||||||
dateList = Object.keys(optionData);
|
|
||||||
|
|
||||||
if (dateList.length > 0) {
|
|
||||||
selectedDate = dateList[0];
|
|
||||||
strikeList = optionData[selectedDate] || [];
|
|
||||||
|
|
||||||
// Select strike closest to current stock price
|
|
||||||
if (strikeList.length > 0) {
|
|
||||||
selectedStrike = strikeList.reduce((closest, strike) => {
|
|
||||||
return Math.abs(strike - currentStockPrice) <
|
|
||||||
Math.abs(closest - currentStockPrice)
|
|
||||||
? strike
|
|
||||||
: closest;
|
|
||||||
}, strikeList[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching stock data:", error);
|
console.error("Error fetching stock data:", error);
|
||||||
}
|
}
|
||||||
@ -689,6 +699,7 @@
|
|||||||
const updatedStrategy = [...userStrategy];
|
const updatedStrategy = [...userStrategy];
|
||||||
updatedStrategy[index].date = selectedDate;
|
updatedStrategy[index].date = selectedDate;
|
||||||
userStrategy = updatedStrategy;
|
userStrategy = updatedStrategy;
|
||||||
|
await loadData("default");
|
||||||
shouldUpdate = true;
|
shouldUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -703,23 +714,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOptionPriceInput(event: Event) {
|
function handleOptionPriceInput(event: Event, index) {
|
||||||
const value = (event.target as HTMLInputElement).value;
|
const value = (event.target as HTMLInputElement).value;
|
||||||
|
|
||||||
selectedOptionPrice = value === "" ? null : +value;
|
selectedOptionPrice = value === "" ? null : +value;
|
||||||
|
const updatedStrategy = [...userStrategy];
|
||||||
|
updatedStrategy[index].optionPrice = selectedOptionPrice;
|
||||||
|
userStrategy = updatedStrategy;
|
||||||
|
|
||||||
// Clear any existing debounce timeout
|
// Clear any existing debounce timeout
|
||||||
if (debounceTimeout) clearTimeout(debounceTimeout);
|
if (debounceTimeout) clearTimeout(debounceTimeout);
|
||||||
|
|
||||||
// Set a new debounce timeout
|
// Set a new debounce timeout
|
||||||
debounceTimeout = setTimeout(() => {
|
debounceTimeout = setTimeout(() => {
|
||||||
if (userStrategy.length > 0) {
|
shouldUpdate = true;
|
||||||
userStrategy = userStrategy.map((leg) => ({
|
|
||||||
...leg,
|
|
||||||
optionPrice: selectedOptionPrice,
|
|
||||||
}));
|
|
||||||
shouldUpdate = true;
|
|
||||||
}
|
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -729,18 +737,15 @@
|
|||||||
|
|
||||||
selectedQuantity = value === "" ? null : +value;
|
selectedQuantity = value === "" ? null : +value;
|
||||||
const updatedStrategy = [...userStrategy];
|
const updatedStrategy = [...userStrategy];
|
||||||
updatedStrategy[index].quantity = updatedStrategy[index].quantity =
|
updatedStrategy[index].quantity = selectedQuantity;
|
||||||
selectedQuantity;
|
|
||||||
userStrategy = updatedStrategy;
|
userStrategy = updatedStrategy;
|
||||||
|
if (debounceTimeout) clearTimeout(debounceTimeout);
|
||||||
|
|
||||||
|
// Set a new debounce timeout
|
||||||
|
debounceTimeout = setTimeout(() => {
|
||||||
|
shouldUpdate = true;
|
||||||
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear any existing debounce timeout
|
|
||||||
if (debounceTimeout) clearTimeout(debounceTimeout);
|
|
||||||
|
|
||||||
// Set a new debounce timeout
|
|
||||||
debounceTimeout = setTimeout(() => {
|
|
||||||
shouldUpdate = true;
|
|
||||||
}, 300);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function search() {
|
async function search() {
|
||||||
@ -779,6 +784,7 @@
|
|||||||
await getStockData();
|
await getStockData();
|
||||||
await loadData("default");
|
await loadData("default");
|
||||||
inputValue = "";
|
inputValue = "";
|
||||||
|
shouldUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// LIFECYCLE FUNCTIONS
|
// LIFECYCLE FUNCTIONS
|
||||||
@ -800,7 +806,10 @@
|
|||||||
$: {
|
$: {
|
||||||
if (shouldUpdate) {
|
if (shouldUpdate) {
|
||||||
shouldUpdate = false;
|
shouldUpdate = false;
|
||||||
|
|
||||||
config = plotData();
|
config = plotData();
|
||||||
|
userStrategy = [...userStrategy];
|
||||||
|
|
||||||
isLoaded = true;
|
isLoaded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1120,7 +1129,7 @@
|
|||||||
step="0.1"
|
step="0.1"
|
||||||
min="0"
|
min="0"
|
||||||
value={userStrategy[index]?.optionPrice}
|
value={userStrategy[index]?.optionPrice}
|
||||||
on:input={handleOptionPriceInput}
|
on:input={(e) => handleOptionPriceInput(e, index)}
|
||||||
class="border border-gray-300 dark:border-gray-500 rounded px-2 py-1 w-24 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
class="border border-gray-300 dark:border-gray-500 rounded px-2 py-1 w-24 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
@ -1239,7 +1248,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-baseline">
|
<div class="flex items-baseline">
|
||||||
<span class="text-lg font-semibold"
|
<span class="text-lg font-semibold"
|
||||||
>${breakEvenPrice?.toFixed(2)}</span
|
>{typeof breakEvenPrice === "number"
|
||||||
|
? "$" + breakEvenPrice?.toFixed(2)
|
||||||
|
: "n/a"}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1289,7 +1300,7 @@
|
|||||||
<div
|
<div
|
||||||
class="text-lg font-semibold text-green-800 dark:text-green-400"
|
class="text-lg font-semibold text-green-800 dark:text-green-400"
|
||||||
>
|
>
|
||||||
{limits?.maxProfit}
|
{metrics?.maxProfit}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1307,7 +1318,7 @@
|
|||||||
<div
|
<div
|
||||||
class="text-lg font-semibold text-red-600 dark:text-red-400"
|
class="text-lg font-semibold text-red-600 dark:text-red-400"
|
||||||
>
|
>
|
||||||
{limits?.maxLoss}
|
{metrics?.maxLoss}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user