update
This commit is contained in:
parent
07638762d3
commit
fe29176176
@ -12,41 +12,67 @@
|
|||||||
import { mode } from "mode-watcher";
|
import { mode } from "mode-watcher";
|
||||||
import highcharts from "$lib/highcharts.ts";
|
import highcharts from "$lib/highcharts.ts";
|
||||||
|
|
||||||
export let data;
|
// Types
|
||||||
let isLoaded = true;
|
type Strategy = {
|
||||||
let shouldUpdate = false;
|
name: string;
|
||||||
|
sentiment: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
let config = null;
|
type OptionLeg = {
|
||||||
|
action: string;
|
||||||
|
quantity: number;
|
||||||
|
date: string;
|
||||||
|
strike: number;
|
||||||
|
optionType: string;
|
||||||
|
optionPrice: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SearchResult = {
|
||||||
|
symbol: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
// State variables with proper types
|
||||||
|
let isLoaded = false;
|
||||||
|
let shouldUpdate = false;
|
||||||
|
let config: any = null;
|
||||||
|
|
||||||
|
// Strategy selection
|
||||||
let selectedStrategy = "Long Call";
|
let selectedStrategy = "Long Call";
|
||||||
let selectedOptionType = "Call";
|
let selectedOptionType = "Call";
|
||||||
let selectedTicker = "TSLA";
|
let selectedTicker = "TSLA";
|
||||||
let assetType = "stocks";
|
let assetType = "stocks";
|
||||||
|
|
||||||
let selectedAction = "Buy";
|
let selectedAction = "Buy";
|
||||||
let selectedOptionPrice;
|
let selectedOptionPrice: number;
|
||||||
let selectedQuantity = 1;
|
let selectedQuantity = 1;
|
||||||
let debounceTimeout;
|
let debounceTimeout: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
let currentStockPrice;
|
// Market data
|
||||||
|
let currentStockPrice: number;
|
||||||
|
let optionData: Record<string, any> = {};
|
||||||
|
let dateList: string[] = [];
|
||||||
|
let selectedDate: string;
|
||||||
|
let strikeList: number[] = [];
|
||||||
|
let selectedStrike: number;
|
||||||
|
|
||||||
let optionData = {};
|
// Option information
|
||||||
let dateList = [];
|
let optionSymbol: string;
|
||||||
let selectedDate;
|
let breakEvenPrice: number | null = null;
|
||||||
let strikeList = [];
|
let totalPremium: number;
|
||||||
let selectedStrike;
|
let limits: Record<string, string> = {};
|
||||||
|
let rawData: Record<string, any> = {};
|
||||||
let optionSymbol;
|
|
||||||
let breakEvenPrice;
|
|
||||||
let totalPremium;
|
|
||||||
let limits = {};
|
|
||||||
let rawData = {};
|
|
||||||
|
|
||||||
let searchBarData = [];
|
|
||||||
let timeoutId;
|
|
||||||
|
|
||||||
|
// Search variables
|
||||||
|
let searchBarData: SearchResult[] = [];
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout>;
|
||||||
let inputValue = "";
|
let inputValue = "";
|
||||||
let touchedInput = false;
|
let touchedInput = false;
|
||||||
let prebuiltStrategy = [
|
|
||||||
|
// Strategy definitions
|
||||||
|
const prebuiltStrategy: Strategy[] = [
|
||||||
{
|
{
|
||||||
name: "Long Call",
|
name: "Long Call",
|
||||||
sentiment: "Bullish",
|
sentiment: "Bullish",
|
||||||
@ -57,7 +83,7 @@
|
|||||||
name: "Long Put",
|
name: "Long Put",
|
||||||
sentiment: "Bearish",
|
sentiment: "Bearish",
|
||||||
description:
|
description:
|
||||||
" In a long put strategy, an investor purchases a put option, expecting that the price of the underlying asset will decrease and generate a profit from the option's increased value. Investors typically use a long put strategy when they have a bearish outlook on the stock.",
|
"In a long put strategy, an investor purchases a put option, expecting that the price of the underlying asset will decrease and generate a profit from the option's increased value. Investors typically use a long put strategy when they have a bearish outlook on the stock.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Short Call",
|
name: "Short Call",
|
||||||
@ -69,56 +95,38 @@
|
|||||||
name: "Short Put",
|
name: "Short Put",
|
||||||
sentiment: "Bullish",
|
sentiment: "Bullish",
|
||||||
description:
|
description:
|
||||||
" In this strategy, an investor sells a put option, expecting that the price of the underlying asset will remain stable or increase, allowing the investor to keep the premium received from selling the option. Investors typically use a short put strategy when they have a neutral to bullish outlook on the stock and and views a potential assignment as an opportunity to buy the asset at a desirable price.",
|
"In this strategy, an investor sells a put option, expecting that the price of the underlying asset will remain stable or increase, allowing the investor to keep the premium received from selling the option. Investors typically use a short put strategy when they have a neutral to bullish outlook on the stock and and views a potential assignment as an opportunity to buy the asset at a desirable price.",
|
||||||
},
|
},
|
||||||
/*
|
// Other strategies commented out in original code
|
||||||
{ name: "Custom Strategy", sentiment: "" },
|
|
||||||
{ name: "Covered Call", sentiment: "Bullish" },
|
|
||||||
{ name: "Protective Put", sentiment: "Bullish" },
|
|
||||||
{ name: "Cash Secured Put", sentiment: "Bullish" },
|
|
||||||
{ name: "Bull Call Spread", sentiment: "Bullish" },
|
|
||||||
{ name: "Bull Put Spread", sentiment: "Bullish" },
|
|
||||||
{ name: "Bear Call Spread", sentiment: "Bearish" },
|
|
||||||
{ name: "Bear Put Spread", sentiment: "Bearish" },
|
|
||||||
{ name: "Collar", sentiment: "Neutral" },
|
|
||||||
{ name: "Iron Condor", sentiment: "Neutral" },
|
|
||||||
{ name: "Calendar Spread", sentiment: "Neutral" },
|
|
||||||
{ name: "Covered Combination", sentiment: "Neutral" },
|
|
||||||
{ name: "Long Call Butterfly", sentiment: "Neutral" },
|
|
||||||
{ name: "Long Straddle", sentiment: "Neutral" },
|
|
||||||
{ name: "Short Straddle", sentiment: "Neutral" },
|
|
||||||
*/
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let userStrategy = [];
|
let userStrategy: OptionLeg[] = [];
|
||||||
|
let description = prebuiltStrategy[0]?.description;
|
||||||
|
|
||||||
let description = prebuiltStrategy?.at(0)?.description;
|
// STRATEGY FUNCTIONS
|
||||||
|
|
||||||
async function changeStrategy(strategy) {
|
async function changeStrategy(strategy: Strategy) {
|
||||||
selectedStrategy = strategy?.name;
|
selectedStrategy = strategy?.name;
|
||||||
description = strategy?.description;
|
description = strategy?.description;
|
||||||
|
|
||||||
|
// Set appropriate option type and action based on strategy
|
||||||
switch (selectedStrategy) {
|
switch (selectedStrategy) {
|
||||||
case "Long Call":
|
case "Long Call":
|
||||||
selectedOptionType = "Call";
|
selectedOptionType = "Call";
|
||||||
selectedAction = "Buy";
|
selectedAction = "Buy";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "Short Call":
|
case "Short Call":
|
||||||
selectedOptionType = "Call";
|
selectedOptionType = "Call";
|
||||||
selectedAction = "Sell";
|
selectedAction = "Sell";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "Long Put":
|
case "Long Put":
|
||||||
selectedOptionType = "Put";
|
selectedOptionType = "Put";
|
||||||
selectedAction = "Buy";
|
selectedAction = "Buy";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "Short Put":
|
case "Short Put":
|
||||||
selectedOptionType = "Put";
|
selectedOptionType = "Put";
|
||||||
selectedAction = "Sell";
|
selectedAction = "Sell";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn("Unknown strategy:", strategy);
|
console.warn("Unknown strategy:", strategy);
|
||||||
selectedOptionType = null;
|
selectedOptionType = null;
|
||||||
@ -128,29 +136,31 @@
|
|||||||
await loadData("default");
|
await loadData("default");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PAYOFF CALCULATION FUNCTIONS
|
||||||
|
|
||||||
const payoffFunctions = {
|
const payoffFunctions = {
|
||||||
"Buy Call": (s, strike, premium) =>
|
"Buy Call": (s: number, strike: number, premium: number) =>
|
||||||
s < strike ? -premium : (s - strike) * 100 * selectedQuantity - premium,
|
s < strike ? -premium : (s - strike) * 100 * selectedQuantity - premium,
|
||||||
|
|
||||||
"Sell Call": (s, strike, premium) =>
|
"Sell Call": (s: number, strike: number, premium: number) =>
|
||||||
s < strike ? premium : premium - (s - strike) * 100 * selectedQuantity,
|
s < strike ? premium : premium - (s - strike) * 100 * selectedQuantity,
|
||||||
|
|
||||||
"Buy Put": (s, strike, premium) =>
|
"Buy Put": (s: number, strike: number, premium: number) =>
|
||||||
s > strike ? -premium : (strike - s) * 100 * selectedQuantity - premium,
|
s > strike ? -premium : (strike - s) * 100 * selectedQuantity - premium,
|
||||||
|
|
||||||
"Sell Put": (s, strike, premium) =>
|
"Sell Put": (s: number, strike: number, premium: number) =>
|
||||||
s > strike ? premium : premium - (strike - s) * 100 * selectedQuantity,
|
s > strike ? premium : premium - (strike - s) * 100 * selectedQuantity,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Define break-even calculators for each scenario (using per-share price)
|
// Define break-even calculators for each scenario (using per-share price)
|
||||||
const breakEvenCalculators = {
|
const breakEvenCalculators = {
|
||||||
"Buy Call": (strike, optionPrice) => strike + optionPrice,
|
"Buy Call": (strike: number, optionPrice: number) => strike + optionPrice,
|
||||||
"Sell Call": (strike, optionPrice) => strike + optionPrice,
|
"Sell Call": (strike: number, optionPrice: number) => strike + optionPrice,
|
||||||
"Buy Put": (strike, optionPrice) => strike - optionPrice,
|
"Buy Put": (strike: number, optionPrice: number) => strike - optionPrice,
|
||||||
"Sell Put": (strike, optionPrice) => strike - optionPrice,
|
"Sell Put": (strike: number, optionPrice: number) => strike - optionPrice,
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
const formatDate = (dateString: string) => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return date.toLocaleDateString("en-US", {
|
return date.toLocaleDateString("en-US", {
|
||||||
month: "short",
|
month: "short",
|
||||||
@ -159,26 +169,21 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function plotData() {
|
// CHART FUNCTIONS
|
||||||
userStrategy = [
|
|
||||||
{
|
|
||||||
action: selectedAction,
|
|
||||||
quantity: selectedQuantity,
|
|
||||||
date: selectedDate,
|
|
||||||
strike: selectedStrike,
|
|
||||||
optionType: selectedOptionType,
|
|
||||||
optionPrice: selectedOptionPrice,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
|
function plotData() {
|
||||||
// Determine x-axis range based on current stock price and max leg strike
|
// Determine x-axis range based on current stock price and max leg strike
|
||||||
const maxLegStrike = Math.max(...userStrategy?.map((leg) => leg.strike));
|
if (!userStrategy || userStrategy.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxLegStrike = Math.max(...userStrategy.map((leg) => leg.strike));
|
||||||
const xMin = 0;
|
const xMin = 0;
|
||||||
const xMax = Math.floor(Math.max(currentStockPrice, maxLegStrike) * 3);
|
const xMax = Math.floor(Math.max(currentStockPrice, maxLegStrike) * 3);
|
||||||
const step = 10;
|
const step = 10;
|
||||||
|
|
||||||
// Calculate the total premium across all legs
|
// Calculate the total premium across all legs
|
||||||
totalPremium = userStrategy?.reduce((sum, leg) => {
|
totalPremium = userStrategy.reduce((sum, leg) => {
|
||||||
return sum + leg.optionPrice * 100 * leg.quantity;
|
return sum + leg.optionPrice * 100 * leg.quantity;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
@ -205,74 +210,10 @@
|
|||||||
dataPoints.push([s, aggregatedPayoff]);
|
dataPoints.push([s, aggregatedPayoff]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userStrategy.length === 1) {
|
// Calculate break-even and limits for single-leg strategies
|
||||||
const leg = userStrategy[0];
|
calculateBreakEvenAndLimits();
|
||||||
const scenarioKey = `${leg?.action} ${leg?.optionType}`;
|
|
||||||
if (breakEvenCalculators[scenarioKey]) {
|
|
||||||
breakEvenPrice = breakEvenCalculators[scenarioKey](
|
|
||||||
leg.strike,
|
|
||||||
leg.optionPrice,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (scenarioKey === "Buy Call") {
|
|
||||||
limits = {
|
|
||||||
maxProfit: "Unlimited",
|
|
||||||
maxLoss: `-$${totalPremium.toLocaleString("en-US", {
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
})}`,
|
|
||||||
};
|
|
||||||
} else if (scenarioKey === "Sell Call") {
|
|
||||||
limits = {
|
|
||||||
maxProfit: `$${totalPremium.toLocaleString("en-US", {
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
})}`,
|
|
||||||
maxLoss: "Unlimited",
|
|
||||||
};
|
|
||||||
} else if (scenarioKey === "Buy Put") {
|
|
||||||
limits = {
|
|
||||||
maxProfit: `$${(leg.strike * 100 - totalPremium).toLocaleString(
|
|
||||||
"en-US",
|
|
||||||
{
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
},
|
|
||||||
)}`,
|
|
||||||
maxLoss: `-$${totalPremium.toLocaleString("en-US", {
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
})}`,
|
|
||||||
};
|
|
||||||
} else if (scenarioKey === "Sell Put") {
|
|
||||||
limits = {
|
|
||||||
maxProfit: `$${totalPremium.toLocaleString("en-US", {
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
})}`,
|
|
||||||
maxLoss: `-$${(leg.strike * 100 - totalPremium).toLocaleString(
|
|
||||||
"en-US",
|
|
||||||
{
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
},
|
|
||||||
)}`,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
console.error("Limits not defined for scenario:", scenarioKey);
|
|
||||||
limits = { maxProfit: "n/a", maxLoss: "n/a" };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For multiple legs, simply display the aggregated premium info
|
|
||||||
limits = {
|
|
||||||
info: `Aggregated Premium: $${totalPremium.toLocaleString("en-US", {
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
})}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the chart options (using the first leg's ticker for the title)
|
// Build the chart options
|
||||||
const options = {
|
const options = {
|
||||||
credits: { enabled: false },
|
credits: { enabled: false },
|
||||||
chart: {
|
chart: {
|
||||||
@ -353,7 +294,8 @@
|
|||||||
const profitLoss = this.y;
|
const profitLoss = this.y;
|
||||||
const underlyingPctChange =
|
const underlyingPctChange =
|
||||||
((underlyingPrice - currentStockPrice) / currentStockPrice) * 100;
|
((underlyingPrice - currentStockPrice) / currentStockPrice) * 100;
|
||||||
const profitLossPctChange = (profitLoss / totalPremium) * 100;
|
const profitLossPctChange =
|
||||||
|
totalPremium !== 0 ? (profitLoss / totalPremium) * 100 : 0;
|
||||||
return `
|
return `
|
||||||
<div class="flex flex-col items-start text-sm">
|
<div class="flex flex-col items-start text-sm">
|
||||||
<div>
|
<div>
|
||||||
@ -403,18 +345,80 @@
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getContractHistory = async (contractId) => {
|
// Calculate break-even price and profit/loss limits
|
||||||
let output;
|
function calculateBreakEvenAndLimits() {
|
||||||
const cachedData = getCache(contractId, "getContractHistory");
|
if (userStrategy.length === 1) {
|
||||||
if (cachedData) {
|
const leg = userStrategy[0];
|
||||||
output = cachedData;
|
const scenarioKey = `${leg?.action} ${leg?.optionType}`;
|
||||||
|
|
||||||
|
// Calculate break-even price
|
||||||
|
if (breakEvenCalculators[scenarioKey]) {
|
||||||
|
breakEvenPrice = breakEvenCalculators[scenarioKey](
|
||||||
|
leg.strike,
|
||||||
|
leg.optionPrice,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
breakEvenPrice = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set profit/loss limits based on strategy
|
||||||
|
if (scenarioKey === "Buy Call") {
|
||||||
|
limits = {
|
||||||
|
maxProfit: "Unlimited",
|
||||||
|
maxLoss: `-$${formatCurrency(totalPremium)}`,
|
||||||
|
};
|
||||||
|
} else if (scenarioKey === "Sell Call") {
|
||||||
|
limits = {
|
||||||
|
maxProfit: `$${formatCurrency(totalPremium)}`,
|
||||||
|
maxLoss: "Unlimited",
|
||||||
|
};
|
||||||
|
} else if (scenarioKey === "Buy Put") {
|
||||||
|
limits = {
|
||||||
|
maxProfit: `$${formatCurrency(leg.strike * 100 - totalPremium)}`,
|
||||||
|
maxLoss: `-$${formatCurrency(totalPremium)}`,
|
||||||
|
};
|
||||||
|
} else if (scenarioKey === "Sell Put") {
|
||||||
|
limits = {
|
||||||
|
maxProfit: `$${formatCurrency(totalPremium)}`,
|
||||||
|
maxLoss: `-$${formatCurrency(leg.strike * 100 - totalPremium)}`,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.error("Limits not defined for scenario:", scenarioKey);
|
||||||
|
limits = { maxProfit: "n/a", maxLoss: "n/a" };
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// For multiple legs, display the aggregated premium info
|
||||||
|
breakEvenPrice = null;
|
||||||
|
limits = {
|
||||||
|
info: `Aggregated Premium: $${formatCurrency(totalPremium)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for currency formatting
|
||||||
|
function formatCurrency(value: number): string {
|
||||||
|
return value.toLocaleString("en-US", {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// DATA LOADING FUNCTIONS
|
||||||
|
|
||||||
|
const getContractHistory = async (contractId: string) => {
|
||||||
|
const cacheKey = contractId;
|
||||||
|
const cachedData = getCache(cacheKey, "getContractHistory");
|
||||||
|
|
||||||
|
if (cachedData) {
|
||||||
|
return cachedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const postData = {
|
const postData = {
|
||||||
ticker: selectedTicker,
|
ticker: selectedTicker,
|
||||||
contract: contractId,
|
contract: contractId,
|
||||||
};
|
};
|
||||||
|
|
||||||
// make the POST request to the endpoint
|
|
||||||
const response = await fetch("/api/options-contract-history", {
|
const response = await fetch("/api/options-contract-history", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@ -423,60 +427,206 @@
|
|||||||
body: JSON.stringify(postData),
|
body: JSON.stringify(postData),
|
||||||
});
|
});
|
||||||
|
|
||||||
output = await response.json();
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch contract history: ${response.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
setCache(contractId, output, "getContractHistory");
|
const output = await response.json();
|
||||||
|
setCache(cacheKey, output, "getContractHistory");
|
||||||
|
return output;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching contract history:", error);
|
||||||
|
return { history: [{ mark: 0 }] };
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function handleOptionType() {
|
async function loadData(state: string) {
|
||||||
if (selectedOptionType === "Call") {
|
if (!rawData?.getData) {
|
||||||
selectedOptionType = "Put";
|
console.error("rawData is undefined or invalid in loadData");
|
||||||
} else {
|
return;
|
||||||
selectedOptionType = "Call";
|
}
|
||||||
|
|
||||||
|
isLoaded = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
optionData = rawData?.getData[selectedOptionType] || {};
|
||||||
|
dateList = Object.keys(optionData);
|
||||||
|
|
||||||
|
// Make sure selectedDate exists in the data
|
||||||
|
if (!dateList.includes(selectedDate) && dateList.length > 0) {
|
||||||
|
selectedDate = dateList[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
strikeList = optionData[selectedDate] || [];
|
||||||
|
|
||||||
|
// Find closest strike to current stock price
|
||||||
|
if (!strikeList.includes(selectedStrike) && strikeList.length > 0) {
|
||||||
|
selectedStrike = strikeList.reduce((closest, strike) => {
|
||||||
|
return Math.abs(strike - currentStockPrice) <
|
||||||
|
Math.abs(closest - currentStockPrice)
|
||||||
|
? strike
|
||||||
|
: closest;
|
||||||
|
}, strikeList[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get option price
|
||||||
|
optionSymbol = buildOptionSymbol(
|
||||||
|
selectedTicker,
|
||||||
|
selectedDate,
|
||||||
|
selectedOptionType,
|
||||||
|
selectedStrike,
|
||||||
|
);
|
||||||
|
|
||||||
|
const output = await getContractHistory(optionSymbol);
|
||||||
|
selectedOptionPrice = output?.history?.at(-1)?.mark || 0;
|
||||||
|
|
||||||
|
// Update user strategy if necessary
|
||||||
|
if (state === "default" && userStrategy.length > 0) {
|
||||||
|
userStrategy = userStrategy.map((leg) => ({
|
||||||
|
...leg,
|
||||||
|
date: selectedDate,
|
||||||
|
strike: selectedStrike,
|
||||||
|
optionType: selectedOptionType,
|
||||||
|
optionPrice: selectedOptionPrice,
|
||||||
|
action: selectedAction,
|
||||||
|
quantity: selectedQuantity,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldUpdate = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading data:", error);
|
||||||
|
} finally {
|
||||||
|
isLoaded = true;
|
||||||
}
|
}
|
||||||
await loadData("optionType");
|
|
||||||
}
|
}
|
||||||
async function handleAction() {
|
|
||||||
if (selectedAction === "Buy") {
|
async function getStockData() {
|
||||||
selectedAction = "Sell";
|
try {
|
||||||
|
const postData = { ticker: selectedTicker };
|
||||||
|
const response = await fetch("/api/options-calculator", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(postData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch stock data: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
rawData = (await response.json()) || {};
|
||||||
|
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) {
|
||||||
|
console.error("Error fetching stock data:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// USER INTERACTION FUNCTIONS
|
||||||
|
|
||||||
|
async function handleAddOptionLeg() {
|
||||||
|
if (userStrategy.length === 0) {
|
||||||
|
userStrategy = [
|
||||||
|
{
|
||||||
|
action: selectedAction,
|
||||||
|
quantity: selectedQuantity,
|
||||||
|
date: selectedDate,
|
||||||
|
strike: selectedStrike,
|
||||||
|
optionType: selectedOptionType,
|
||||||
|
optionPrice: selectedOptionPrice,
|
||||||
|
},
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
selectedAction = "Buy";
|
const lastLeg = userStrategy[userStrategy.length - 1];
|
||||||
|
const newLeg = { ...lastLeg }; // Create a shallow copy
|
||||||
|
userStrategy = [...userStrategy, newLeg];
|
||||||
}
|
}
|
||||||
shouldUpdate = true;
|
shouldUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOptionPriceInput(event) {
|
async function handleOptionType() {
|
||||||
if (event.target.value === "") {
|
selectedOptionType = selectedOptionType === "Call" ? "Put" : "Call";
|
||||||
selectedOptionPrice = "";
|
await loadData("optionType");
|
||||||
} else {
|
|
||||||
selectedOptionPrice = +event.target.value;
|
|
||||||
}
|
|
||||||
// Clear any existing debounce timeout
|
|
||||||
if (debounceTimeout) clearTimeout(debounceTimeout);
|
|
||||||
|
|
||||||
// Set a new debounce timeout (1 second)
|
|
||||||
debounceTimeout = setTimeout(() => {
|
|
||||||
config = plotData();
|
|
||||||
}, 500);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleQuantityInput(event) {
|
// FIXED: Make sure the handleAction function correctly uses the index
|
||||||
// Check if the input is empty
|
async function handleAction(index: number) {
|
||||||
if (event.target.value === "") {
|
if (index !== undefined && userStrategy[index]) {
|
||||||
selectedQuantity = "";
|
// Update the specific leg in userStrategy
|
||||||
|
const updatedStrategy = [...userStrategy];
|
||||||
|
updatedStrategy[index].action =
|
||||||
|
updatedStrategy[index].action === "Buy" ? "Sell" : "Buy";
|
||||||
|
userStrategy = updatedStrategy;
|
||||||
} else {
|
} else {
|
||||||
selectedQuantity = +event.target.value;
|
// Update the selectedAction (for new legs)
|
||||||
|
selectedAction = selectedAction === "Buy" ? "Sell" : "Buy";
|
||||||
}
|
}
|
||||||
|
console.log(userStrategy);
|
||||||
|
shouldUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOptionPriceInput(event: Event) {
|
||||||
|
const value = (event.target as HTMLInputElement).value;
|
||||||
|
|
||||||
|
selectedOptionPrice = value === "" ? null : +value;
|
||||||
|
|
||||||
// Clear any existing debounce timeout
|
// Clear any existing debounce timeout
|
||||||
if (debounceTimeout) clearTimeout(debounceTimeout);
|
if (debounceTimeout) clearTimeout(debounceTimeout);
|
||||||
|
|
||||||
// Set a new debounce timeout (1 second)
|
// Set a new debounce timeout
|
||||||
debounceTimeout = setTimeout(() => {
|
debounceTimeout = setTimeout(() => {
|
||||||
config = plotData();
|
if (userStrategy.length > 0) {
|
||||||
}, 500);
|
userStrategy = userStrategy.map((leg) => ({
|
||||||
|
...leg,
|
||||||
|
optionPrice: selectedOptionPrice,
|
||||||
|
}));
|
||||||
|
shouldUpdate = true;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleQuantityInput(event: Event) {
|
||||||
|
const value = (event.target as HTMLInputElement).value;
|
||||||
|
|
||||||
|
selectedQuantity = value === "" ? null : +value;
|
||||||
|
|
||||||
|
// Clear any existing debounce timeout
|
||||||
|
if (debounceTimeout) clearTimeout(debounceTimeout);
|
||||||
|
|
||||||
|
// Set a new debounce timeout
|
||||||
|
debounceTimeout = setTimeout(() => {
|
||||||
|
if (userStrategy.length > 0) {
|
||||||
|
userStrategy = userStrategy.map((leg) => ({
|
||||||
|
...leg,
|
||||||
|
quantity: selectedQuantity,
|
||||||
|
}));
|
||||||
|
shouldUpdate = true;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function search() {
|
async function search() {
|
||||||
@ -489,98 +639,65 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
timeoutId = setTimeout(async () => {
|
timeoutId = setTimeout(async () => {
|
||||||
const response = await fetch(
|
try {
|
||||||
`/api/searchbar?query=${encodeURIComponent(inputValue)}&limit=10`,
|
const response = await fetch(
|
||||||
);
|
`/api/searchbar?query=${encodeURIComponent(inputValue)}&limit=10`,
|
||||||
searchBarData = await response?.json();
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Search failed: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchBarData = await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during search:", error);
|
||||||
|
searchBarData = [];
|
||||||
|
}
|
||||||
}, 50); // delay
|
}, 50); // delay
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadData(state: string) {
|
async function changeTicker(data: SearchResult) {
|
||||||
if (!rawData || !rawData.getData) {
|
if (!data?.symbol) return;
|
||||||
console.error("rawData is undefined or invalid in loadData");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoaded = false;
|
selectedTicker = data.symbol;
|
||||||
|
|
||||||
optionData = rawData?.getData[selectedOptionType];
|
|
||||||
|
|
||||||
dateList = [...Object?.keys(optionData)];
|
|
||||||
|
|
||||||
strikeList = [...optionData[selectedDate]];
|
|
||||||
|
|
||||||
if (!strikeList?.includes(selectedStrike)) {
|
|
||||||
selectedStrike = strikeList.reduce((closest, strike) => {
|
|
||||||
return Math.abs(strike - currentStockPrice) <
|
|
||||||
Math.abs(closest - currentStockPrice)
|
|
||||||
? strike
|
|
||||||
: closest;
|
|
||||||
}, strikeList[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
optionSymbol = buildOptionSymbol(
|
|
||||||
selectedTicker,
|
|
||||||
selectedDate,
|
|
||||||
selectedOptionType,
|
|
||||||
selectedStrike,
|
|
||||||
);
|
|
||||||
const output = await getContractHistory(optionSymbol);
|
|
||||||
|
|
||||||
selectedOptionPrice = output?.history?.at(-1)?.mark;
|
|
||||||
shouldUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getStockData() {
|
|
||||||
const postData = { ticker: selectedTicker };
|
|
||||||
const response = await fetch("/api/options-calculator", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(postData),
|
|
||||||
});
|
|
||||||
|
|
||||||
rawData = (await response.json()) || {};
|
|
||||||
|
|
||||||
currentStockPrice = rawData?.getStockQuote?.price;
|
|
||||||
|
|
||||||
optionData = rawData?.getData[selectedOptionType];
|
|
||||||
dateList = Object?.keys(optionData);
|
|
||||||
selectedDate = Object?.keys(optionData)[0];
|
|
||||||
strikeList = optionData[selectedDate] || [];
|
|
||||||
selectedStrike = strikeList.reduce((closest, strike) => {
|
|
||||||
return Math.abs(strike - currentStockPrice) <
|
|
||||||
Math.abs(closest - currentStockPrice)
|
|
||||||
? strike
|
|
||||||
: closest;
|
|
||||||
}, strikeList[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function changeTicker(data) {
|
|
||||||
selectedTicker = data?.symbol;
|
|
||||||
assetType = data?.type?.toLowerCase() || "stocks";
|
assetType = data?.type?.toLowerCase() || "stocks";
|
||||||
|
|
||||||
await getStockData();
|
await getStockData();
|
||||||
await loadData("default");
|
await loadData("default");
|
||||||
inputValue = "";
|
inputValue = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LIFECYCLE FUNCTIONS
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await getStockData();
|
await getStockData();
|
||||||
await loadData("default");
|
await loadData("default");
|
||||||
|
|
||||||
|
userStrategy = [
|
||||||
|
{
|
||||||
|
action: selectedAction,
|
||||||
|
quantity: selectedQuantity,
|
||||||
|
date: selectedDate,
|
||||||
|
strike: selectedStrike,
|
||||||
|
optionType: selectedOptionType,
|
||||||
|
optionPrice: selectedOptionPrice,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
shouldUpdate = true;
|
shouldUpdate = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (debounceTimeout) clearTimeout(debounceTimeout);
|
if (debounceTimeout) clearTimeout(debounceTimeout);
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// REACTIVE STATEMENTS
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (shouldUpdate) {
|
if (shouldUpdate) {
|
||||||
shouldUpdate = false;
|
shouldUpdate = false;
|
||||||
|
|
||||||
config = plotData();
|
config = plotData();
|
||||||
|
|
||||||
isLoaded = true;
|
isLoaded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -590,6 +707,13 @@
|
|||||||
config = plotData();
|
config = plotData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watch for changes to inputValue and trigger search
|
||||||
|
$: {
|
||||||
|
if (inputValue) {
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SEO
|
<SEO
|
||||||
@ -705,7 +829,7 @@
|
|||||||
class="overflow-x-auto border border-gray-300 dark:border-gray-600 rounded"
|
class="overflow-x-auto border border-gray-300 dark:border-gray-600 rounded"
|
||||||
>
|
>
|
||||||
<table
|
<table
|
||||||
class="min-w-full divide-y divide-gray-200 dark:divide-gray-600"
|
class="min-w-full divide-y divide-gray-200 dark:divide-gray-600 bg-[#F8F9FA] dark:bg-secondary"
|
||||||
>
|
>
|
||||||
<!-- Table head -->
|
<!-- Table head -->
|
||||||
<thead class="bg-gray-50 dark:bg-secondary">
|
<thead class="bg-gray-50 dark:bg-secondary">
|
||||||
@ -763,14 +887,14 @@
|
|||||||
<tbody
|
<tbody
|
||||||
class="bg-[#F8F9FA] dark:bg-secondary divide-y divide-gray-200 dark:divide-gray-800 text-sm"
|
class="bg-[#F8F9FA] dark:bg-secondary divide-y divide-gray-200 dark:divide-gray-800 text-sm"
|
||||||
>
|
>
|
||||||
{#each userStrategy as item}
|
{#each userStrategy as item, index}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-4 py-3 whitespace-nowrap font-semibold">
|
<td class="px-4 whitespace-nowrap font-semibold">
|
||||||
{selectedTicker}
|
{selectedTicker}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 whitespace-nowrap">
|
<td class="px-4 whitespace-nowrap">
|
||||||
<label
|
<label
|
||||||
on:click={handleAction}
|
on:click={() => handleAction(index)}
|
||||||
class="badge px-2 select-none rounded-md {item?.action ===
|
class="badge px-2 select-none rounded-md {item?.action ===
|
||||||
'Buy'
|
'Buy'
|
||||||
? 'bg-green-100 text-green-800 dark:bg-green-300 dark:text-muted'
|
? 'bg-green-100 text-green-800 dark:bg-green-300 dark:text-muted'
|
||||||
@ -778,7 +902,7 @@
|
|||||||
>{item?.action}</label
|
>{item?.action}</label
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 whitespace-nowrap">
|
<td class="px-4 whitespace-nowrap">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
bind:value={selectedQuantity}
|
bind:value={selectedQuantity}
|
||||||
@ -787,7 +911,7 @@
|
|||||||
class="border border-gray-300 dark:border-gray-500 rounded px-2 py-1 w-20 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-20 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 whitespace-nowrap">
|
<td class="px-4 whitespace-nowrap">
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger asChild let:builder>
|
<DropdownMenu.Trigger asChild let:builder>
|
||||||
<Button
|
<Button
|
||||||
@ -833,7 +957,7 @@
|
|||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 whitespace-nowrap">
|
<td class="px-4 whitespace-nowrap">
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger asChild let:builder>
|
<DropdownMenu.Trigger asChild let:builder>
|
||||||
<Button
|
<Button
|
||||||
@ -880,14 +1004,14 @@
|
|||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 whitespace-nowrap">
|
<td class="px-4 whitespace-nowrap">
|
||||||
<label
|
<label
|
||||||
on:click={handleOptionType}
|
on:click={handleOptionType}
|
||||||
class="select-none badge px-2 rounded-md bg-blue-100 text-blue-800 dark:bg-blue-300 dark:text-muted font-semibold cursor-pointer"
|
class="select-none badge px-2 rounded-md bg-blue-100 text-blue-800 dark:bg-blue-300 dark:text-muted font-semibold cursor-pointer"
|
||||||
>{item?.optionType}</label
|
>{item?.optionType}</label
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 whitespace-nowrap">
|
<td class="px-4 whitespace-nowrap">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
@ -897,7 +1021,7 @@
|
|||||||
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>
|
||||||
<td class="px-4 py-3 whitespace-nowrap">
|
<td class="px-4 whitespace-nowrap">
|
||||||
<a
|
<a
|
||||||
href={`/${["stocks", "stock"]?.includes(assetType) ? "stocks" : assetType === "etf" ? "etf" : "index"}/${selectedTicker}/options/contract-lookup?query=${optionSymbol}`}
|
href={`/${["stocks", "stock"]?.includes(assetType) ? "stocks" : assetType === "etf" ? "etf" : "index"}/${selectedTicker}/options/contract-lookup?query=${optionSymbol}`}
|
||||||
class="option-leg-link-to-contract"
|
class="option-leg-link-to-contract"
|
||||||
@ -910,7 +1034,25 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<!-- Add more rows as needed -->
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={() => handleAddOptionLeg()}
|
||||||
|
class="cursor-pointer mt-3 mb-3 ml-3 align-middle inline-flex items-center gap-x-1.5 rounded bg-green-600 px-2.5 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600 transition duration-150 ease-in-out whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="-ml-0.5 h-4 w-4"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
Add Option Leg
|
||||||
|
</button>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user