// ==UserScript==
// @name Kraken Pro Trade Helper
// @namespace http://tampermonkey.net/
// @version 1.4
// @description Calculate profitable selling prices for trades on Kraken Pro.
// @author Imran Pollob
// @license MIT
// @match https://pro.kraken.com/*
// @grant none
// @require https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js
// ==/UserScript==
function tradeSummaryWithFractional(coinPrice, investmentAmount, currentCoinPrice = null) {
// Convert parameters to floats
coinPrice = parseFloat(coinPrice);
investmentAmount = parseFloat(investmentAmount);
currentCoinPrice = currentCoinPrice ? parseFloat(currentCoinPrice) : null;
const anchorElement = document.querySelector('a[href*="fee-level"]');
const spanElements = anchorElement.querySelectorAll(".text-ds-primary.text-ds-labelMono3");
const makerFee = spanElements[0].firstElementChild.textContent;
const feePercent = parseFloat(makerFee);
// console.log(coinPrice, investmentAmount, currentCoinPrice, feePercent);
// Input validation
if (coinPrice <= 0 || investmentAmount <= 0 || feePercent < 0) {
throw new Error("Invalid input: coinPrice, investmentAmount must be > 0 and feePercent >= 0");
}
// Step 1: Calculate the fractional quantity bought
const quantity = investmentAmount / coinPrice;
const buyFee = investmentAmount * (feePercent / 100); // Fee for buying
const actualTotalCost = investmentAmount + buyFee;
const actualPricePerUnit = actualTotalCost / quantity;
// Step 2: Break-even Selling Price
const breakEvenPrice = actualPricePerUnit / (1 - feePercent / 100);
// Step 3: Calculate Target Prices for Gains
const targetPrices = {};
const profits = {};
const netSellValues = {};
const gains = [0.5, 1, 2, 3, 4, 5];
for (const gain of gains) {
const targetPrice = breakEvenPrice * (1 + gain / 100);
const sellFeeAtTarget = targetPrice * quantity * (feePercent / 100);
const totalSellValueAtTarget = targetPrice * quantity - sellFeeAtTarget;
const profitAtTarget = totalSellValueAtTarget - actualTotalCost;
targetPrices[gain] = targetPrice;
profits[gain] = profitAtTarget;
netSellValues[gain] = totalSellValueAtTarget;
}
// Step 4: Profit/Loss at Current Coin Price (if provided)
let profitAtCurrent;
let percentageProfitLoss;
let totalSellValueAtCurrent;
if (currentCoinPrice) {
const sellFeeAtCurrent = currentCoinPrice * quantity * (feePercent / 100); // Sell fee at current price
totalSellValueAtCurrent = currentCoinPrice * quantity - sellFeeAtCurrent; // Net sell value
profitAtCurrent = totalSellValueAtCurrent - actualTotalCost; // Profit or loss at current price
percentageProfitLoss = (profitAtCurrent / actualTotalCost) * 100; // Percentage profit/loss
} else {
profitAtCurrent = null;
percentageProfitLoss = null;
totalSellValueAtCurrent = null;
}
// Prepare results for React
const results = [];
for (const gain in targetPrices) {
results.push({
percentage: gain,
price: targetPrices[gain],
net: profits[gain],
});
}
results.sort((a, b) => parseFloat(a.percentage) - parseFloat(b.percentage));
if (currentCoinPrice) {
results.unshift({
percentage: percentageProfitLoss,
price: currentCoinPrice,
net: profitAtCurrent,
});
}
return [breakEvenPrice, results];
}
(function () {
"use strict";
const initReactApp = () => {
// Ensure React and ReactDOM are available globally
const React = window.React;
const ReactDOM = window.ReactDOM;
if (!React || !ReactDOM) {
console.error("React or ReactDOM not loaded properly.");
return;
}
const ResultRow2 = ({ label, value }) => {
return React.createElement(
"div",
{ className: "flex justify-between items-center h-4" },
React.createElement(
"div",
{ className: "text-ds text-ds-body3 ms-ds-0 me-ds-2 mt-ds-0 mb-ds-0" },
label
),
React.createElement(
"div",
{ className: "text-ds text-ds-body3 ms-ds-0 me-ds-0 mt-ds-0 mb-ds-0" },
value
)
);
};
const ResultRow3 = ({ percentage, price, net }) => {
return React.createElement(
"div",
{ className: "flex justify-between items-center h-4" },
React.createElement(
"div",
{ className: "text-ds text-ds-body3 ms-ds-0 me-ds-2 mt-ds-0 mb-ds-0" },
percentage
),
React.createElement(
"div",
{ className: "text-ds text-ds-body3 ms-ds-0 me-ds-2 mt-ds-0 mb-ds-0" },
price
),
React.createElement("div", { className: "text-ds text-ds-body3 ms-ds-0 me-ds-0 mt-ds-0 mb-ds-0" }, net)
);
};
// React Component
const CryptoProfitCalculator = () => {
const [coinPrice, setCoinPrice] = React.useState(0);
const [totalInvested, setTotalInvested] = React.useState(0);
const [boughtPrice, setBoughtPrice] = React.useState("");
const [breakEvenPrice, setBreakEvenPrice] = React.useState(0);
const [results, setResults] = React.useState(null);
const [fractionLength, setFractionLength] = React.useState(4);
// Fetch input values from the page
const fetchInputValues = () => {
const existingCoinPrice = parseFloat(document.querySelector('[id^="price-"]')?.value || 0);
if (existingCoinPrice.toString().includes(".")) {
let fractionLength = existingCoinPrice.toString().split(".")[1]?.length;
fractionLength = Math.max(fractionLength, 4);
}
setFractionLength(fractionLength);
const existingTotalInvested = parseFloat(document.querySelector('[id^="volumeInQuote-"]')?.value || 0);
setCoinPrice(existingCoinPrice);
setTotalInvested(existingTotalInvested);
};
// Fetch initial values on component mount
React.useEffect(() => {
fetchInputValues(); // Fetch initial values
}, []); // Empty dependency array ensures this runs only once
// Observe DOM changes
React.useEffect(() => {
const coinPriceElement = document.querySelector('[id^="price-"]');
const totalInvestedElement = document.querySelector('[id^="volumeInQuote-"]');
if (coinPriceElement && totalInvestedElement) {
const observer = new MutationObserver(fetchInputValues);
observer.observe(coinPriceElement, { attributes: true, attributeFilter: ["value"] });
observer.observe(totalInvestedElement, { attributes: true, attributeFilter: ["value"] });
return () => observer.disconnect();
}
}, []);
// Update calculations when values change
React.useEffect(() => {
const parsedBoughtPrice = boughtPrice !== "" ? parseFloat(boughtPrice) : null;
if (coinPrice > 0 && totalInvested > 0) {
const [calculatedBreakEvenPrice, calculatedResults] = tradeSummaryWithFractional(
coinPrice,
totalInvested,
boughtPrice
);
setBreakEvenPrice(calculatedBreakEvenPrice);
setResults(calculatedResults);
}
}, [coinPrice, totalInvested, boughtPrice]);
const formatCurrency = (value, digits = fractionLength, locale = "en-US", currency = "USD") => {
return new Intl.NumberFormat(locale, {
style: "decimal",
currency: currency,
minimumFractionDigits: digits,
maximumFractionDigits: digits,
}).format(value);
};
return React.createElement(
"div",
{ className: "flex flex-col gap-y-2" },
// React.createElement(ResultRow2, { label: "Coin Price", value: coinPrice }),
// React.createElement(ResultRow2, { label: "Total", value: totalInvested }),
React.createElement(ResultRow2, { label: "Break Even Price:", value: formatCurrency(breakEvenPrice) }),
React.createElement(ResultRow2, {
label: React.createElement("label", { htmlFor: "bought-price" }, "Expected Price:"),
value: React.createElement("input", {
type: "number",
id: "bought-price",
value: boughtPrice,
onChange: (e) => setBoughtPrice(e.target.value),
style: {
backgroundColor: "#333",
color: "#fff",
border: "1px solid rgba(255, 255, 255, 0.2)", // Optional styling
},
}),
}),
results &&
React.createElement(
"div",
{ className: "flex flex-col gap-y-2" },
results.map((result, index) =>
React.createElement(ResultRow3, {
key: index,
percentage: `${parseFloat(result.percentage).toFixed(2)}%`,
price: formatCurrency(result.price),
net: formatCurrency(result.net, 2),
})
)
)
);
};
// Target a container in the DOM
const targetContainer = document.querySelector("form.flex.overflow-auto.flex-col");
if (targetContainer) {
const appContainer = document.createElement("div");
appContainer.id = "trading-helper";
appContainer.classList.add(
"ms-ds-0",
"me-ds-0",
"mt-ds-0",
"mb-ds-0",
"rounded-ds-5",
"relative",
"inline-block",
"outline-offset-2",
"border-ds-card",
"bg-ds-card",
"outline-none",
"p-3",
"w-full",
"box-border",
"ds-card",
"bg-transparent",
"border",
"!border-[#686B8229]"
);
targetContainer.appendChild(appContainer);
ReactDOM.render(React.createElement(CryptoProfitCalculator), appContainer);
} else {
console.error("Target container not found.");
}
};
const initializeScript = () => {
const observer = new MutationObserver((mutations, obs) => {
const targetElement = document.querySelector('a[href*="fee-level"]');
if (targetElement) {
if (!document.getElementById("trading-helper")) {
console.log("Fee level element found. Initializing...");
initReactApp();
}
obs.disconnect();
} else {
console.log("Fee level element not found. Observing...");
}
});
// Start observing the entire document body for changes
observer.observe(document.body, { childList: true, subtree: true });
};
// Initial script execution
initializeScript();
// Detect URL changes
const observeUrlChanges = () => {
let currentUrl = window.location.href;
// Observe for changes in history state (pushState/replaceState)
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function (...args) {
originalPushState.apply(this, args);
window.dispatchEvent(new Event("urlchange"));
};
history.replaceState = function (...args) {
originalReplaceState.apply(this, args);
window.dispatchEvent(new Event("urlchange"));
};
// Listen for back/forward navigation (popstate)
window.addEventListener("popstate", () => {
if (currentUrl !== window.location.href) {
currentUrl = window.location.href;
initializeScript();
}
});
// Listen for custom 'urlchange' event
window.addEventListener("urlchange", () => {
if (currentUrl !== window.location.href) {
currentUrl = window.location.href;
initializeScript();
}
});
};
// Start observing URL changes
observeUrlChanges();
})();