mirror of
https://github.com/Gerald-Ha/HodlEye-Crypto-Price-Tracker.git
synced 2025-06-25 17:16:27 +00:00
Compare commits
No commits in common. "819f36f26feaf61f3176c121c32395dfdc800f86" and "214bb5c3d1e06330f9f004f1a1ddde9a420afdda" have entirely different histories.
819f36f26f
...
214bb5c3d1
@ -1,19 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
hodleye:
|
|
||||||
build: .
|
|
||||||
image: hodleye-crypto-tracker:latest
|
|
||||||
container_name: hodleye-container
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
ports:
|
|
||||||
- "3099:3099"
|
|
||||||
- "5001:5001"
|
|
||||||
volumes:
|
|
||||||
- hodleye_data:/app/data
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
hodleye_data:
|
|
||||||
external: true
|
|
@ -205,26 +205,3 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.note-button, .edit-button {
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: 5px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-button.has-note {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-button.no-note {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-input-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-input-row textarea {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
@ -68,7 +68,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="version-info">
|
<div class="version-info">
|
||||||
<span id="currentVersion">Version 1.5.1</span>
|
<span id="currentVersion">Version 1.5.0</span>
|
||||||
<span
|
<span
|
||||||
id="updateAvailable"
|
id="updateAvailable"
|
||||||
style="display: none; color: red; cursor: pointer"
|
style="display: none; color: red; cursor: pointer"
|
||||||
|
@ -1,37 +1,24 @@
|
|||||||
let portfolioData = [];
|
let portfolioData = [];
|
||||||
let coinColors = {};
|
let coinColors = {};
|
||||||
let addTransactionModal, editTransactionModal, colorConfigModal, noteModal;
|
let addTransactionModal, editTransactionModal, colorConfigModal;
|
||||||
let buyForm, sellForm;
|
let buyForm, sellForm;
|
||||||
let portfolioList, pieChartCanvas, chartLegend;
|
let portfolioList, pieChartCanvas, chartLegend;
|
||||||
let editIdGlobal = null;
|
let editIdGlobal = null;
|
||||||
let currentNoteId = null;
|
|
||||||
let currentNoteType = null;
|
|
||||||
|
|
||||||
function formatPrice(value) {
|
|
||||||
if (isNaN(value)) return "-";
|
|
||||||
if (value < 0.01) return value.toFixed(6);
|
|
||||||
if (value < 1) return value.toFixed(4);
|
|
||||||
return value.toFixed(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
addTransactionModal = document.getElementById("addTransactionModal");
|
addTransactionModal = document.getElementById("addTransactionModal");
|
||||||
editTransactionModal = document.getElementById("editTransactionModal");
|
editTransactionModal = document.getElementById("editTransactionModal");
|
||||||
colorConfigModal = document.getElementById("colorConfigModal");
|
colorConfigModal = document.getElementById("colorConfigModal");
|
||||||
noteModal = document.getElementById("noteModal");
|
|
||||||
buyForm = document.getElementById("buyForm");
|
buyForm = document.getElementById("buyForm");
|
||||||
sellForm = document.getElementById("sellForm");
|
sellForm = document.getElementById("sellForm");
|
||||||
portfolioList = document.getElementById("portfolioList");
|
portfolioList = document.getElementById("portfolioList");
|
||||||
pieChartCanvas = document.getElementById("portfolioPieChart");
|
pieChartCanvas = document.getElementById("portfolioPieChart");
|
||||||
chartLegend = document.getElementById("chartLegend");
|
chartLegend = document.getElementById("chartLegend");
|
||||||
|
|
||||||
window.addEventListener("click", (event) => {
|
window.addEventListener("click", (event) => {
|
||||||
if (event.target === addTransactionModal) closeAddTransactionModal();
|
if (event.target === addTransactionModal) closeAddTransactionModal();
|
||||||
if (event.target === editTransactionModal) closeEditTransactionModal();
|
if (event.target === editTransactionModal) closeEditTransactionModal();
|
||||||
if (event.target === colorConfigModal) closeColorConfigModal();
|
if (event.target === colorConfigModal) closeColorConfigModal();
|
||||||
if (event.target === noteModal) closeNoteModal();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
loadCoinColorsFromStorage();
|
loadCoinColorsFromStorage();
|
||||||
loadPortfolioData();
|
loadPortfolioData();
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
@ -68,14 +55,12 @@ function renderPortfolio() {
|
|||||||
if (!grouped[t.symbol]) grouped[t.symbol] = [];
|
if (!grouped[t.symbol]) grouped[t.symbol] = [];
|
||||||
grouped[t.symbol].push(t);
|
grouped[t.symbol].push(t);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let sym in grouped) {
|
for (let sym in grouped) {
|
||||||
const section = document.createElement("div");
|
const section = document.createElement("div");
|
||||||
section.className = "coin-section";
|
section.className = "coin-section";
|
||||||
const heading = document.createElement("h2");
|
const heading = document.createElement("h2");
|
||||||
heading.textContent = sym;
|
heading.textContent = sym;
|
||||||
section.appendChild(heading);
|
section.appendChild(heading);
|
||||||
|
|
||||||
const table = document.createElement("table");
|
const table = document.createElement("table");
|
||||||
const thead = document.createElement("thead");
|
const thead = document.createElement("thead");
|
||||||
thead.innerHTML = `
|
thead.innerHTML = `
|
||||||
@ -91,11 +76,9 @@ function renderPortfolio() {
|
|||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
table.appendChild(thead);
|
table.appendChild(thead);
|
||||||
|
|
||||||
const tbody = document.createElement("tbody");
|
const tbody = document.createElement("tbody");
|
||||||
let totalAmount = 0;
|
let totalAmount = 0;
|
||||||
let totalCost = 0;
|
let totalCost = 0;
|
||||||
|
|
||||||
grouped[sym].forEach(tx => {
|
grouped[sym].forEach(tx => {
|
||||||
const currentTotal = tx.amount * tx.currentPrice;
|
const currentTotal = tx.amount * tx.currentPrice;
|
||||||
const initialTotal = tx.amount * tx.buyPrice;
|
const initialTotal = tx.amount * tx.buyPrice;
|
||||||
@ -103,40 +86,33 @@ function renderPortfolio() {
|
|||||||
const pct = initialTotal === 0 ? 0 : (diff / initialTotal) * 100;
|
const pct = initialTotal === 0 ? 0 : (diff / initialTotal) * 100;
|
||||||
totalAmount += tx.amount;
|
totalAmount += tx.amount;
|
||||||
totalCost += initialTotal;
|
totalCost += initialTotal;
|
||||||
|
|
||||||
const row = document.createElement("tr");
|
const row = document.createElement("tr");
|
||||||
const diffClass = diff >= 0 ? "positive" : "negative";
|
const diffClass = diff >= 0 ? "positive" : "negative";
|
||||||
const editButton = `<span class="edit-button" onclick="openEditTransactionModal('${tx.id}')">✎</span>`;
|
const editButton = `<span class="edit-button" onclick="openEditTransactionModal('${tx.id}')">✎</span>`;
|
||||||
const noteButton = `<span class="note-button ${tx.note ? 'has-note' : 'no-note'}" onclick="openNoteModal('${tx.id}', 'portfolio')">✉</span>`;
|
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td>${tx.symbol}</td>
|
<td>${tx.symbol}</td>
|
||||||
<td>${tx.amount}</td>
|
<td>${tx.amount}</td>
|
||||||
<td>${formatPrice(tx.buyPrice)}</td>
|
<td>${tx.buyPrice}</td>
|
||||||
<td>${formatPrice(tx.currentPrice)}</td>
|
<td>${tx.currentPrice.toFixed(2)}</td>
|
||||||
<td>${(tx.amount * tx.buyPrice).toFixed(2)}</td>
|
<td>${(tx.amount * tx.buyPrice).toFixed(2)}</td>
|
||||||
<td class="${diffClass}">${diff.toFixed(2)}</td>
|
<td class="${diffClass}">${diff.toFixed(2)}</td>
|
||||||
<td class="${diffClass}">${pct.toFixed(2)}%</td>
|
<td class="${diffClass}">${pct.toFixed(2)}%</td>
|
||||||
<td>${tx.date}${editButton}${noteButton}</td>
|
<td>${tx.date}${editButton}</td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalCurrent = totalAmount * (grouped[sym][0].currentPrice || 0);
|
const totalCurrent = totalAmount * (grouped[sym][0].currentPrice || 0);
|
||||||
const totalDiff = totalCurrent - totalCost;
|
const totalDiff = totalCurrent - totalCost;
|
||||||
const totalPct = totalCost === 0 ? 0 : (totalDiff / totalCost) * 100;
|
const totalPct = totalCost === 0 ? 0 : (totalDiff / totalCost) * 100;
|
||||||
const avgBuyPrice = totalAmount === 0 ? 0 : totalCost / totalAmount;
|
const avgBuyPrice = totalAmount === 0 ? 0 : totalCost / totalAmount;
|
||||||
|
|
||||||
const totalRow = document.createElement("tr");
|
const totalRow = document.createElement("tr");
|
||||||
totalRow.className = "summary-row";
|
totalRow.className = "summary-row";
|
||||||
const summaryClass = totalDiff >= 0 ? "positive" : "negative";
|
const summaryClass = totalDiff >= 0 ? "positive" : "negative";
|
||||||
totalRow.innerHTML = `
|
totalRow.innerHTML = `
|
||||||
<td>${sym} (Total)</td>
|
<td>${sym} (Total)</td>
|
||||||
<td>${totalAmount.toFixed(6)}</td>
|
<td>${totalAmount.toFixed(6)}</td>
|
||||||
|
<td>Ø ${avgBuyPrice.toFixed(2)}</td>
|
||||||
<td>Ø ${formatPrice(avgBuyPrice)}</td>
|
<td>${(grouped[sym][0].currentPrice || 0).toFixed(2)}</td>
|
||||||
|
|
||||||
<td>${formatPrice(grouped[sym][0].currentPrice || 0)}</td>
|
|
||||||
<td>${totalCost.toFixed(2)}</td>
|
<td>${totalCost.toFixed(2)}</td>
|
||||||
<td class="${summaryClass}">${totalDiff.toFixed(2)}</td>
|
<td class="${summaryClass}">${totalDiff.toFixed(2)}</td>
|
||||||
<td class="${summaryClass}">${totalPct.toFixed(2)}%</td>
|
<td class="${summaryClass}">${totalPct.toFixed(2)}%</td>
|
||||||
@ -171,7 +147,6 @@ function drawPieChart() {
|
|||||||
if (!grouped[t.symbol]) grouped[t.symbol] = 0;
|
if (!grouped[t.symbol]) grouped[t.symbol] = 0;
|
||||||
grouped[t.symbol] += t.currentPrice * t.amount;
|
grouped[t.symbol] += t.currentPrice * t.amount;
|
||||||
});
|
});
|
||||||
|
|
||||||
let totalAll = 0;
|
let totalAll = 0;
|
||||||
let coinValues = [];
|
let coinValues = [];
|
||||||
for (let s in grouped) {
|
for (let s in grouped) {
|
||||||
@ -180,14 +155,12 @@ function drawPieChart() {
|
|||||||
for (let s in grouped) {
|
for (let s in grouped) {
|
||||||
coinValues.push({ symbol: s, value: grouped[s] });
|
coinValues.push({ symbol: s, value: grouped[s] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctx = pieChartCanvas.getContext("2d");
|
const ctx = pieChartCanvas.getContext("2d");
|
||||||
ctx.clearRect(0, 0, pieChartCanvas.width, pieChartCanvas.height);
|
ctx.clearRect(0, 0, pieChartCanvas.width, pieChartCanvas.height);
|
||||||
const centerX = pieChartCanvas.width / 2;
|
const centerX = pieChartCanvas.width / 2;
|
||||||
const centerY = pieChartCanvas.height / 2;
|
const centerY = pieChartCanvas.height / 2;
|
||||||
const radius = Math.min(centerX, centerY) - 10;
|
const radius = Math.min(centerX, centerY) - 10;
|
||||||
let startAngle = 0;
|
let startAngle = 0;
|
||||||
|
|
||||||
coinValues.forEach(item => {
|
coinValues.forEach(item => {
|
||||||
const sliceAngle = totalAll === 0 ? 0 : (item.value / totalAll) * 2 * Math.PI;
|
const sliceAngle = totalAll === 0 ? 0 : (item.value / totalAll) * 2 * Math.PI;
|
||||||
if (!coinColors[item.symbol]) {
|
if (!coinColors[item.symbol]) {
|
||||||
@ -201,7 +174,6 @@ function drawPieChart() {
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
startAngle += sliceAngle;
|
startAngle += sliceAngle;
|
||||||
});
|
});
|
||||||
|
|
||||||
renderChartLegend(coinValues, totalAll);
|
renderChartLegend(coinValues, totalAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,50 +191,6 @@ function renderChartLegend(coinValues, totalAll) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function openNoteModal(id, type) {
|
|
||||||
currentNoteId = id;
|
|
||||||
currentNoteType = type;
|
|
||||||
noteModal.style.display = "block";
|
|
||||||
|
|
||||||
if (type === 'portfolio') {
|
|
||||||
const tx = portfolioData.find(x => String(x.id) === String(id));
|
|
||||||
document.getElementById("noteText").value = tx?.note || "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeNoteModal() {
|
|
||||||
noteModal.style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveNote() {
|
|
||||||
const noteText = document.getElementById("noteText").value;
|
|
||||||
const endpoint = currentNoteType === 'portfolio'
|
|
||||||
? "/api/portfolio/note"
|
|
||||||
: "/api/trade_summary/note";
|
|
||||||
|
|
||||||
fetch(endpoint, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: currentNoteId,
|
|
||||||
note: noteText
|
|
||||||
}),
|
|
||||||
credentials: "include"
|
|
||||||
})
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(j => {
|
|
||||||
if (j.error) {
|
|
||||||
alert(j.error);
|
|
||||||
} else {
|
|
||||||
closeNoteModal();
|
|
||||||
updatePrices(); //
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => alert("Error saving note"));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function openAddTransactionModal() {
|
function openAddTransactionModal() {
|
||||||
addTransactionModal.style.display = "block";
|
addTransactionModal.style.display = "block";
|
||||||
showBuyForm();
|
showBuyForm();
|
||||||
@ -288,12 +216,10 @@ function confirmBuy() {
|
|||||||
const amount = parseFloat(document.getElementById("buyAmount").value);
|
const amount = parseFloat(document.getElementById("buyAmount").value);
|
||||||
const buyPrice = parseFloat(document.getElementById("buyPrice").value);
|
const buyPrice = parseFloat(document.getElementById("buyPrice").value);
|
||||||
const buyDate = document.getElementById("buyDate").value;
|
const buyDate = document.getElementById("buyDate").value;
|
||||||
|
|
||||||
if (!symbol || isNaN(amount) || isNaN(buyPrice)) {
|
if (!symbol || isNaN(amount) || isNaN(buyPrice)) {
|
||||||
alert("Bitte gültige Werte eingeben");
|
alert("Bitte gültige Werte eingeben");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newId = Date.now() + Math.floor(Math.random() * 999999);
|
const newId = Date.now() + Math.floor(Math.random() * 999999);
|
||||||
fetch("/api/coinPrice?symbol=" + symbol, { credentials: "include" })
|
fetch("/api/coinPrice?symbol=" + symbol, { credentials: "include" })
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
@ -331,22 +257,18 @@ function confirmSell() {
|
|||||||
const sel = document.getElementById("sellSelectTransaction");
|
const sel = document.getElementById("sellSelectTransaction");
|
||||||
const val = sel.value;
|
const val = sel.value;
|
||||||
if (!val) return;
|
if (!val) return;
|
||||||
|
|
||||||
const sellAmount = parseFloat(document.getElementById("sellAmount").value);
|
const sellAmount = parseFloat(document.getElementById("sellAmount").value);
|
||||||
const sellPrice = parseFloat(document.getElementById("sellPrice").value);
|
const sellPrice = parseFloat(document.getElementById("sellPrice").value);
|
||||||
const sellDate = document.getElementById("sellDate").value;
|
const sellDate = document.getElementById("sellDate").value;
|
||||||
|
|
||||||
if (isNaN(sellAmount) || isNaN(sellPrice) || !sellDate) {
|
if (isNaN(sellAmount) || isNaN(sellPrice) || !sellDate) {
|
||||||
alert("Bitte gültige Werte eingeben");
|
alert("Bitte gültige Werte eingeben");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = val.split("|");
|
const parts = val.split("|");
|
||||||
const sym = parts[0];
|
const sym = parts[0];
|
||||||
const idx = parseInt(parts[1], 10);
|
const idx = parseInt(parts[1], 10);
|
||||||
let selectedTransaction = null;
|
let selectedTransaction = null;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
for (let i = 0; i < portfolioData.length; i++) {
|
for (let i = 0; i < portfolioData.length; i++) {
|
||||||
if (portfolioData[i].symbol === sym) {
|
if (portfolioData[i].symbol === sym) {
|
||||||
if (count === idx) {
|
if (count === idx) {
|
||||||
@ -356,7 +278,6 @@ function confirmSell() {
|
|||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedTransaction) {
|
if (!selectedTransaction) {
|
||||||
alert("Ungültig");
|
alert("Ungültig");
|
||||||
return;
|
return;
|
||||||
@ -393,7 +314,6 @@ function populateSellDropdown() {
|
|||||||
if (!grouped[t.symbol]) grouped[t.symbol] = [];
|
if (!grouped[t.symbol]) grouped[t.symbol] = [];
|
||||||
grouped[t.symbol].push(t);
|
grouped[t.symbol].push(t);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let sym in grouped) {
|
for (let sym in grouped) {
|
||||||
grouped[sym].forEach((tx, i) => {
|
grouped[sym].forEach((tx, i) => {
|
||||||
const opt = document.createElement("option");
|
const opt = document.createElement("option");
|
||||||
@ -428,16 +348,13 @@ function saveEditedTransaction() {
|
|||||||
alert("Keine gültige Transaktion gefunden");
|
alert("Keine gültige Transaktion gefunden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newAmount = parseFloat(document.getElementById("editAmount").value);
|
const newAmount = parseFloat(document.getElementById("editAmount").value);
|
||||||
const newBuyPrice = parseFloat(document.getElementById("editBuyPrice").value);
|
const newBuyPrice = parseFloat(document.getElementById("editBuyPrice").value);
|
||||||
const newDate = document.getElementById("editDate").value;
|
const newDate = document.getElementById("editDate").value;
|
||||||
|
|
||||||
if (isNaN(newAmount) || isNaN(newBuyPrice)) {
|
if (isNaN(newAmount) || isNaN(newBuyPrice)) {
|
||||||
alert("Bitte gültige Werte eingeben");
|
alert("Bitte gültige Werte eingeben");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch("/api/portfolio/edit", {
|
fetch("/api/portfolio/edit", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@ -463,25 +380,11 @@ function saveEditedTransaction() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteTransaction() {
|
function deleteTransaction() {
|
||||||
openDeleteConfirmationModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
function openDeleteConfirmationModal() {
|
|
||||||
document.getElementById("deleteConfirmationModal").style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeDeleteConfirmationModal() {
|
|
||||||
document.getElementById("deleteConfirmationModal").style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmDeleteTransaction() {
|
|
||||||
let t = portfolioData.find(x => String(x.id) === String(editIdGlobal));
|
let t = portfolioData.find(x => String(x.id) === String(editIdGlobal));
|
||||||
if (!t) {
|
if (!t) {
|
||||||
alert("No valid transaction found");
|
alert("Keine gültige Transaktion gefunden");
|
||||||
closeDeleteConfirmationModal();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch("/api/portfolio/delete", {
|
fetch("/api/portfolio/delete", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@ -493,12 +396,11 @@ function confirmDeleteTransaction() {
|
|||||||
if (j.error) {
|
if (j.error) {
|
||||||
alert(j.error);
|
alert(j.error);
|
||||||
} else {
|
} else {
|
||||||
closeDeleteConfirmationModal();
|
|
||||||
closeEditTransactionModal();
|
closeEditTransactionModal();
|
||||||
updatePrices();
|
updatePrices();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => alert("Error deleting transaction"));
|
.catch(() => alert("Fehler bei Delete"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function openColorConfigModal() {
|
function openColorConfigModal() {
|
||||||
@ -515,14 +417,12 @@ function renderColorConfigList() {
|
|||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
let syms = {};
|
let syms = {};
|
||||||
portfolioData.forEach(t => { syms[t.symbol] = true; });
|
portfolioData.forEach(t => { syms[t.symbol] = true; });
|
||||||
|
|
||||||
Object.keys(syms).forEach(s => {
|
Object.keys(syms).forEach(s => {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.style.marginBottom = "0.5rem";
|
div.style.marginBottom = "0.5rem";
|
||||||
const label = document.createElement("label");
|
const label = document.createElement("label");
|
||||||
label.textContent = s + ": ";
|
label.textContent = s + ": ";
|
||||||
div.appendChild(label);
|
div.appendChild(label);
|
||||||
|
|
||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
input.type = "color";
|
input.type = "color";
|
||||||
if (!coinColors[s]) {
|
if (!coinColors[s]) {
|
||||||
@ -567,4 +467,3 @@ function loadCoinColorsFromStorage() {
|
|||||||
function saveCoinColorsToStorage() {
|
function saveCoinColorsToStorage() {
|
||||||
localStorage.setItem("coinColors", JSON.stringify(coinColors));
|
localStorage.setItem("coinColors", JSON.stringify(coinColors));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ document.querySelectorAll('.modal').forEach(modal => {
|
|||||||
credentials: "same-origin"
|
credentials: "same-origin"
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
if (response.redirected) {
|
if (response.redirected) {
|
||||||
window.location.href = response.url;
|
window.location.href = response.url; // Leitet zur Login-Seite um
|
||||||
}
|
}
|
||||||
}).catch(error => console.error("Logout-Fehler:", error));
|
}).catch(error => console.error("Logout-Fehler:", error));
|
||||||
}
|
}
|
@ -1,13 +1,6 @@
|
|||||||
let allTrades = [];
|
let allTrades = [];
|
||||||
let editTradeId = null;
|
|
||||||
let currentNoteId = null;
|
|
||||||
let currentNoteType = null;
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
noteModal = document.getElementById("noteModal");
|
|
||||||
window.addEventListener("click", (event) => {
|
|
||||||
if (event.target === noteModal) closeNoteModal();
|
|
||||||
});
|
|
||||||
loadTradeSummary();
|
loadTradeSummary();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -72,9 +65,6 @@ function renderTradeSummary(trades) {
|
|||||||
let row = document.createElement("tr");
|
let row = document.createElement("tr");
|
||||||
let profitClass = trade.profit >= 0 ? "positive" : "negative";
|
let profitClass = trade.profit >= 0 ? "positive" : "negative";
|
||||||
let invest = trade.buyPrice * trade.amount;
|
let invest = trade.buyPrice * trade.amount;
|
||||||
const editButton = `<span class="edit-button" onclick="openEditTradeModal('${trade.id}')">✎</span>`;
|
|
||||||
const noteButton = `<span class="note-button ${trade.note ? 'has-note' : 'no-note'}" onclick="openNoteModal('${trade.id}', 'trade')">✉</span>`;
|
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td>${trade.symbol}</td>
|
<td>${trade.symbol}</td>
|
||||||
<td>${trade.amount}</td>
|
<td>${trade.amount}</td>
|
||||||
@ -84,7 +74,7 @@ function renderTradeSummary(trades) {
|
|||||||
<td class="${profitClass}">${trade.profit.toFixed(2)}</td>
|
<td class="${profitClass}">${trade.profit.toFixed(2)}</td>
|
||||||
<td class="${profitClass}">${trade.percentProfit.toFixed(2)}%</td>
|
<td class="${profitClass}">${trade.percentProfit.toFixed(2)}%</td>
|
||||||
<td>${trade.buyDate}</td>
|
<td>${trade.buyDate}</td>
|
||||||
<td>${trade.sellDate}${editButton}${noteButton}</td>
|
<td>${trade.sellDate}</td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
|
|
||||||
@ -142,142 +132,6 @@ function updateBottomBar(trades) {
|
|||||||
document.getElementById("totalTradePercentChange").textContent = pctChange.toFixed(2) + "%";
|
document.getElementById("totalTradePercentChange").textContent = pctChange.toFixed(2) + "%";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function openNoteModal(id, type) {
|
|
||||||
currentNoteId = id;
|
|
||||||
currentNoteType = type;
|
|
||||||
document.getElementById("noteModal").style.display = "block";
|
|
||||||
|
|
||||||
const trade = allTrades.find(x => String(x.id) === String(id));
|
|
||||||
document.getElementById("noteText").value = trade?.note || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeNoteModal() {
|
|
||||||
document.getElementById("noteModal").style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveNote() {
|
|
||||||
const noteText = document.getElementById("noteText").value;
|
|
||||||
|
|
||||||
fetch("/api/trade_summary/note", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: currentNoteId,
|
|
||||||
note: noteText
|
|
||||||
}),
|
|
||||||
credentials: "include"
|
|
||||||
})
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(j => {
|
|
||||||
if (j.error) {
|
|
||||||
alert(j.error);
|
|
||||||
} else {
|
|
||||||
closeNoteModal();
|
|
||||||
loadTradeSummary();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => alert("Error saving note"));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function openEditTradeModal(id) {
|
|
||||||
editTradeId = id;
|
|
||||||
const trade = allTrades.find(t => String(t.id) === String(id));
|
|
||||||
if (!trade) {
|
|
||||||
alert("Invalid trade");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("editTradeAmount").value = trade.amount;
|
|
||||||
document.getElementById("editTradeBuyPrice").value = trade.buyPrice;
|
|
||||||
document.getElementById("editTradeSellPrice").value = trade.sellPrice;
|
|
||||||
document.getElementById("editTradeBuyDate").value = trade.buyDate;
|
|
||||||
document.getElementById("editTradeSellDate").value = trade.sellDate;
|
|
||||||
|
|
||||||
document.getElementById("editTradeModal").style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeEditTradeModal() {
|
|
||||||
document.getElementById("editTradeModal").style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveEditedTrade() {
|
|
||||||
const trade = allTrades.find(t => String(t.id) === String(editTradeId));
|
|
||||||
if (!trade) {
|
|
||||||
alert("Invalid trade");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const amount = parseFloat(document.getElementById("editTradeAmount").value);
|
|
||||||
const buyPrice = parseFloat(document.getElementById("editTradeBuyPrice").value);
|
|
||||||
const sellPrice = parseFloat(document.getElementById("editTradeSellPrice").value);
|
|
||||||
const buyDate = document.getElementById("editTradeBuyDate").value;
|
|
||||||
const sellDate = document.getElementById("editTradeSellDate").value;
|
|
||||||
|
|
||||||
if (isNaN(amount) || isNaN(buyPrice) || isNaN(sellPrice)) {
|
|
||||||
alert("Please enter valid values");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch("/api/trade_summary/edit", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: editTradeId,
|
|
||||||
amount,
|
|
||||||
buyPrice,
|
|
||||||
sellPrice,
|
|
||||||
buyDate,
|
|
||||||
sellDate
|
|
||||||
}),
|
|
||||||
credentials: "include"
|
|
||||||
})
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(j => {
|
|
||||||
if (j.error) {
|
|
||||||
alert(j.error);
|
|
||||||
} else {
|
|
||||||
closeEditTradeModal();
|
|
||||||
loadTradeSummary();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => alert("Error saving trade"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteTrade() {
|
|
||||||
openDeleteTradeConfirmationModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
function openDeleteTradeConfirmationModal() {
|
|
||||||
document.getElementById("deleteTradeConfirmationModal").style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeDeleteTradeConfirmationModal() {
|
|
||||||
document.getElementById("deleteTradeConfirmationModal").style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmDeleteTrade() {
|
|
||||||
fetch("/api/trade_summary/delete", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ id: editTradeId }),
|
|
||||||
credentials: "include"
|
|
||||||
})
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(j => {
|
|
||||||
if (j.error) {
|
|
||||||
alert(j.error);
|
|
||||||
} else {
|
|
||||||
closeDeleteTradeConfirmationModal();
|
|
||||||
closeEditTradeModal();
|
|
||||||
loadTradeSummary();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => alert("Error deleting trade"));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function openDateFilterModal() {
|
function openDateFilterModal() {
|
||||||
document.getElementById("dateFilterModal").style.display = "block";
|
document.getElementById("dateFilterModal").style.display = "block";
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const CURRENT_VERSION = "1.5.1";
|
const CURRENT_VERSION = "1.5.0";
|
||||||
|
|
||||||
function getUpdateUrl() {
|
function getUpdateUrl() {
|
||||||
return "/api/update?t=" + new Date().getTime();
|
return "/api/update?t=" + new Date().getTime();
|
||||||
|
@ -22,17 +22,6 @@
|
|||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.note-button, .edit-button {
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: 5px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.note-button.has-note {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
.note-button.no-note {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -133,24 +122,14 @@
|
|||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<span class="close" onclick="closeEditTransactionModal()">×</span>
|
<span class="close" onclick="closeEditTransactionModal()">×</span>
|
||||||
<h2>Edit / Delete Transaction</h2>
|
<h2>Edit / Delete Transaction</h2>
|
||||||
|
<label for="editSymbol">Symbol:</label>
|
||||||
<div class="form-row">
|
<input type="text" id="editSymbol" disabled />
|
||||||
<label for="editSymbol">Symbol:</label>
|
<label for="editAmount">Amount:</label>
|
||||||
<input type="text" id="editSymbol" disabled />
|
<input type="number" step="0.000001" id="editAmount" />
|
||||||
</div>
|
<label for="editBuyPrice">Buy Price (USDT):</label>
|
||||||
<div class="form-row">
|
<input type="number" step="0.01" id="editBuyPrice" />
|
||||||
<label for="editAmount">Amount:</label>
|
<label for="editDate">Date:</label>
|
||||||
<input type="number" step="0.000001" id="editAmount" />
|
<input type="date" id="editDate" />
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
|
||||||
<label for="editBuyPrice">Buy Price (USDT):</label>
|
|
||||||
<input type="number" step="0.01" id="editBuyPrice" />
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
|
||||||
<label for="editDate">Date:</label>
|
|
||||||
<input type="date" id="editDate" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="edit-buttons">
|
<div class="edit-buttons">
|
||||||
<button onclick="saveEditedTransaction()">Save</button>
|
<button onclick="saveEditedTransaction()">Save</button>
|
||||||
<button onclick="deleteTransaction()">Delete</button>
|
<button onclick="deleteTransaction()">Delete</button>
|
||||||
@ -158,17 +137,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="noteModal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<span class="close" onclick="closeNoteModal()">×</span>
|
|
||||||
<h2>Note</h2>
|
|
||||||
<div class="note-input-row">
|
|
||||||
<textarea id="noteText" rows="4" cols="50"></textarea>
|
|
||||||
<button onclick="saveNote()">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="colorConfigModal" class="modal">
|
<div id="colorConfigModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<span class="close" onclick="closeColorConfigModal()">×</span>
|
<span class="close" onclick="closeColorConfigModal()">×</span>
|
||||||
@ -177,16 +145,5 @@
|
|||||||
<button onclick="saveColorConfig()">OK</button>
|
<button onclick="saveColorConfig()">OK</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="deleteConfirmationModal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<span class="close" onclick="closeDeleteConfirmationModal()">×</span>
|
|
||||||
<h2>Delete Transaction</h2>
|
|
||||||
<p>Are you sure you want to delete this transaction?</p>
|
|
||||||
<div class="edit-buttons">
|
|
||||||
<button onclick="confirmDeleteTransaction()">Delete</button>
|
|
||||||
<button onclick="closeDeleteConfirmationModal()">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
<title>Portfolio: Trade Summary</title>
|
<title>Portfolio: Trade Summary</title>
|
||||||
<link rel="stylesheet" href="css/portfolio.css" />
|
<link rel="stylesheet" href="css/portfolio.css" />
|
||||||
<script defer src="js/trade_summary.js"></script>
|
<script defer src="js/trade_summary.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -22,17 +21,6 @@
|
|||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.note-button, .edit-button {
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: 5px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.note-button.has-note {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
.note-button.no-note {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -44,8 +32,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid-middle">
|
<div class="grid-middle">
|
||||||
<button onclick="window.location.href='portfolio.html'">Live Portfolio</button>
|
<button onclick="window.location.href='portfolio.html'">Live Portfolio</button>
|
||||||
<button onclick="window.location.href='portfolio.html'">Add Transaction</button>
|
|
||||||
<button>Trade Summary</button>
|
<button>Trade Summary</button>
|
||||||
|
<button onclick="window.location.href='portfolio.html'">Add Transaction</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-right">
|
<div class="grid-right">
|
||||||
<button onclick="window.location.href='/logout'">Logout</button>
|
<button onclick="window.location.href='/logout'">Logout</button>
|
||||||
@ -94,59 +82,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="editTradeModal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<span class="close" onclick="closeEditTradeModal()">×</span>
|
|
||||||
<h2>Edit Trade</h2>
|
|
||||||
<div class="form-row">
|
|
||||||
<label for="editTradeAmount">Amount:</label>
|
|
||||||
<input type="number" step="0.000001" id="editTradeAmount" />
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
|
||||||
<label for="editTradeBuyPrice">Buy Price (USDT):</label>
|
|
||||||
<input type="number" step="0.01" id="editTradeBuyPrice" />
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
|
||||||
<label for="editTradeSellPrice">Sell Price (USDT):</label>
|
|
||||||
<input type="number" step="0.01" id="editTradeSellPrice" />
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
|
||||||
<label for="editTradeBuyDate">Buy Date:</label>
|
|
||||||
<input type="date" id="editTradeBuyDate" />
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
|
||||||
<label for="editTradeSellDate">Sell Date:</label>
|
|
||||||
<input type="date" id="editTradeSellDate" />
|
|
||||||
</div>
|
|
||||||
<div class="edit-buttons">
|
|
||||||
<button onclick="saveEditedTrade()">Save</button>
|
|
||||||
<button onclick="deleteTrade()">Delete</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="noteModal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<span class="close" onclick="closeNoteModal()">×</span>
|
|
||||||
<h2>Note</h2>
|
|
||||||
<div class="note-input-row">
|
|
||||||
<textarea id="noteText" rows="4" cols="50"></textarea>
|
|
||||||
<button onclick="saveNote()">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="deleteTradeConfirmationModal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<span class="close" onclick="closeDeleteTradeConfirmationModal()">×</span>
|
|
||||||
<h2>Delete Trade</h2>
|
|
||||||
<p>Are you sure you want to delete this trade?</p>
|
|
||||||
<div class="edit-buttons">
|
|
||||||
<button onclick="confirmDeleteTrade()">Delete</button>
|
|
||||||
<button onclick="closeDeleteTradeConfirmationModal()">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
const express = require("express");
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
const DATA_FILE = path.join(__dirname, "..", "data", "data.json");
|
|
||||||
|
|
||||||
function readData() {
|
|
||||||
return JSON.parse(fs.readFileSync(DATA_FILE, "utf8"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeData(data) {
|
|
||||||
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
router.get("/", (req, res) => {
|
|
||||||
try {
|
|
||||||
const data = readData();
|
|
||||||
res.json(data.alarms);
|
|
||||||
} catch (err) {
|
|
||||||
res.status(500).json({ error: "Fehler beim Lesen der Daten" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post("/", (req, res) => {
|
|
||||||
const { symbol, price, frequency, direction } = req.body;
|
|
||||||
if (!symbol || !price) {
|
|
||||||
return res.status(400).json({ error: "Symbol und Preis sind erforderlich." });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const data = readData();
|
|
||||||
const newAlarm = {
|
|
||||||
id: Date.now(),
|
|
||||||
symbol: symbol.toUpperCase(),
|
|
||||||
price: parseFloat(price),
|
|
||||||
frequency: frequency || "Once",
|
|
||||||
direction: direction || "Rising",
|
|
||||||
triggered: false
|
|
||||||
};
|
|
||||||
data.alarms.push(newAlarm);
|
|
||||||
writeData(data);
|
|
||||||
res.json(newAlarm);
|
|
||||||
} catch (err) {
|
|
||||||
res.status(500).json({ error: "Fehler beim Schreiben der Daten" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.delete("/:id", (req, res) => {
|
|
||||||
const alarmId = parseInt(req.params.id, 10);
|
|
||||||
try {
|
|
||||||
const data = readData();
|
|
||||||
data.alarms = data.alarms.filter(a => a.id !== alarmId);
|
|
||||||
writeData(data);
|
|
||||||
res.json({ success: true, alarms: data.alarms });
|
|
||||||
} catch (err) {
|
|
||||||
res.status(500).json({ error: "Fehler beim Löschen des Alarms" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
288
server/server.js
288
server/server.js
@ -10,7 +10,7 @@ const fetch = require("node-fetch");
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Metadata
|
* Metadata
|
||||||
* Version: 1.5.1
|
* Version: 1.5.0
|
||||||
* Author/Dev: Gerald Hasani
|
* Author/Dev: Gerald Hasani
|
||||||
* Name: HodlEye Crypto Price Tracker
|
* Name: HodlEye Crypto Price Tracker
|
||||||
* Email: contact@gerald-hasani.com
|
* Email: contact@gerald-hasani.com
|
||||||
@ -58,10 +58,6 @@ app.get("/logout", (req, res) => {
|
|||||||
res.redirect("/login");
|
res.redirect("/login");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
const wss = new WebSocket.Server({ server });
|
const wss = new WebSocket.Server({ server });
|
||||||
const clients = new Set();
|
const clients = new Set();
|
||||||
@ -187,97 +183,43 @@ app.put("/api/cryptos", (req, res) => {
|
|||||||
res.json({ success: true, cryptos: data.cryptos });
|
res.json({ success: true, cryptos: data.cryptos });
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ------------- Notizen ------------- */
|
app.get("/api/alarms", (req, res) => {
|
||||||
|
const data = readData();
|
||||||
app.post("/api/portfolio/note", (req, res) => {
|
res.json(data.alarms);
|
||||||
const { id, note } = req.body;
|
|
||||||
const pf = readPortfolio();
|
|
||||||
const tx = pf.transactions.find((x) => String(x.id) === String(id));
|
|
||||||
if (!tx) return res.status(400).json({ error: "Invalid ID" });
|
|
||||||
tx.note = note;
|
|
||||||
writePortfolio(pf);
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/api/trade_summary/note", (req, res) => {
|
app.post("/api/alarms", (req, res) => {
|
||||||
const { id, note } = req.body;
|
const { symbol, price, frequency, direction } = req.body;
|
||||||
const ts = readTradeSummary();
|
if (!symbol || !price) {
|
||||||
const trade = ts.trades.find((x) => String(x.id) === String(id));
|
return res.status(400).json({ error: "symbol and price are required." });
|
||||||
if (!trade) return res.status(400).json({ error: "Invalid ID" });
|
}
|
||||||
trade.note = note;
|
const data = readData();
|
||||||
writeTradeSummary(ts);
|
const newAlarm = {
|
||||||
res.json({ success: true });
|
id: Date.now(),
|
||||||
|
symbol: symbol.toUpperCase(),
|
||||||
|
price: parseFloat(price),
|
||||||
|
frequency: frequency || "Once",
|
||||||
|
direction: direction || "Rising",
|
||||||
|
triggered: false
|
||||||
|
};
|
||||||
|
data.alarms.push(newAlarm);
|
||||||
|
writeData(data);
|
||||||
|
res.json(newAlarm);
|
||||||
});
|
});
|
||||||
|
|
||||||
const alarmsRouter = require("./alarms");
|
app.delete("/api/alarms/:id", (req, res) => {
|
||||||
app.use("/api/alarms", alarmsRouter);
|
const alarmId = parseInt(req.params.id, 10);
|
||||||
|
const data = readData();
|
||||||
|
data.alarms = data.alarms.filter(a => a.id !== alarmId);
|
||||||
|
writeData(data);
|
||||||
|
res.json({ success: true, alarms: data.alarms });
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/api/notifications", (req, res) => {
|
app.get("/api/notifications", (req, res) => {
|
||||||
const data = readData();
|
const data = readData();
|
||||||
res.json(data.notifications);
|
res.json(data.notifications);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
app.post("/api/portfolio/note", (req, res) => {
|
|
||||||
const { id, note } = req.body;
|
|
||||||
const pf = readPortfolio();
|
|
||||||
const tx = pf.transactions.find(x => String(x.id) === String(id));
|
|
||||||
|
|
||||||
if (!tx) {
|
|
||||||
return res.status(400).json({ error: "Invalid ID" });
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.note = note;
|
|
||||||
writePortfolio(pf);
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/api/trade_summary/note", (req, res) => {
|
|
||||||
const { id, note } = req.body;
|
|
||||||
const ts = readTradeSummary();
|
|
||||||
const trade = ts.trades.find(x => String(x.id) === String(id));
|
|
||||||
if (!trade) {
|
|
||||||
return res.status(400).json({ error: "Invalid ID" });
|
|
||||||
}
|
|
||||||
trade.note = note;
|
|
||||||
writeTradeSummary(ts);
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/api/trade_summary/edit", (req, res) => {
|
|
||||||
const { id, amount, buyPrice, sellPrice, buyDate, sellDate } = req.body;
|
|
||||||
const ts = readTradeSummary();
|
|
||||||
const trade = ts.trades.find(x => String(x.id) === String(id));
|
|
||||||
if (!trade) {
|
|
||||||
return res.status(400).json({ error: "Invalid ID" });
|
|
||||||
}
|
|
||||||
|
|
||||||
trade.amount = parseFloat(amount);
|
|
||||||
trade.buyPrice = parseFloat(buyPrice);
|
|
||||||
trade.sellPrice = parseFloat(sellPrice);
|
|
||||||
trade.buyDate = buyDate;
|
|
||||||
trade.sellDate = sellDate;
|
|
||||||
trade.profit = (trade.sellPrice - trade.buyPrice) * trade.amount;
|
|
||||||
trade.percentProfit = trade.buyPrice > 0
|
|
||||||
? (trade.profit / (trade.buyPrice * trade.amount)) * 100
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
writeTradeSummary(ts);
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/api/trade_summary/delete", (req, res) => {
|
|
||||||
const { id } = req.body;
|
|
||||||
const ts = readTradeSummary();
|
|
||||||
const before = ts.trades.length;
|
|
||||||
ts.trades = ts.trades.filter(t => String(t.id) !== String(id));
|
|
||||||
if (ts.trades.length === before) {
|
|
||||||
return res.status(400).json({ error: "No trade with given ID" });
|
|
||||||
}
|
|
||||||
writeTradeSummary(ts);
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/api/notifications", (req, res) => {
|
app.post("/api/notifications", (req, res) => {
|
||||||
const { message } = req.body;
|
const { message } = req.body;
|
||||||
if (!message) {
|
if (!message) {
|
||||||
@ -381,53 +323,6 @@ app.get("/api/portfolio", (req, res) => {
|
|||||||
res.json(data);
|
res.json(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/api/portfolio/buy", async (req, res) => {
|
|
||||||
const { id, symbol, amount, buyPrice, date } = req.body;
|
|
||||||
if (!symbol || !amount || !buyPrice) {
|
|
||||||
return res.status(400).json({ error: "Invalid data" });
|
|
||||||
}
|
|
||||||
const upSym = symbol.toUpperCase();
|
|
||||||
const binSup = await binanceSupported(upSym);
|
|
||||||
const okxSup = await okxSupported(upSym);
|
|
||||||
if (!binSup && !okxSup) {
|
|
||||||
return res.status(400).json({ error: "Coin not supported" });
|
|
||||||
}
|
|
||||||
let realPrice;
|
|
||||||
if (binSup) {
|
|
||||||
try {
|
|
||||||
realPrice = await getBinancePrice(upSym);
|
|
||||||
} catch {
|
|
||||||
realPrice = parseFloat(buyPrice);
|
|
||||||
}
|
|
||||||
} else if (okxSup) {
|
|
||||||
try {
|
|
||||||
realPrice = await getOkxPrice(upSym);
|
|
||||||
} catch {
|
|
||||||
realPrice = parseFloat(buyPrice);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
realPrice = parseFloat(buyPrice);
|
|
||||||
}
|
|
||||||
const pf = readPortfolio();
|
|
||||||
const transactionId = id ? String(id) : String(Date.now() + Math.floor(Math.random() * 999999));
|
|
||||||
pf.transactions.push({
|
|
||||||
id: transactionId,
|
|
||||||
symbol: upSym,
|
|
||||||
amount: parseFloat(amount),
|
|
||||||
buyPrice: parseFloat(buyPrice),
|
|
||||||
date: date || new Date().toISOString().split("T")[0],
|
|
||||||
currentPrice: realPrice,
|
|
||||||
note: ""
|
|
||||||
});
|
|
||||||
writePortfolio(pf);
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/api/portfolio", (req, res) => {
|
|
||||||
const data = readPortfolio();
|
|
||||||
res.json(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/api/portfolio/buy", async (req, res) => {
|
app.post("/api/portfolio/buy", async (req, res) => {
|
||||||
const { id, symbol, amount, buyPrice, date } = req.body;
|
const { id, symbol, amount, buyPrice, date } = req.body;
|
||||||
if (!symbol || !amount || !buyPrice) {
|
if (!symbol || !amount || !buyPrice) {
|
||||||
@ -469,12 +364,10 @@ app.post("/api/portfolio/buy", async (req, res) => {
|
|||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
app.get("/api/portfolio/prices", async (req, res) => {
|
app.get("/api/portfolio/prices", async (req, res) => {
|
||||||
const pf = readPortfolio();
|
const pf = readPortfolio();
|
||||||
let result = [];
|
let result = [];
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
for (let t of pf.transactions) {
|
for (let t of pf.transactions) {
|
||||||
if (!t.id) {
|
if (!t.id) {
|
||||||
t.id = String(Date.now() + Math.floor(Math.random() * 999999));
|
t.id = String(Date.now() + Math.floor(Math.random() * 999999));
|
||||||
@ -482,29 +375,29 @@ app.get("/api/portfolio/prices", async (req, res) => {
|
|||||||
} else {
|
} else {
|
||||||
t.id = String(t.id);
|
t.id = String(t.id);
|
||||||
}
|
}
|
||||||
|
let price = t.currentPrice;
|
||||||
try {
|
try {
|
||||||
const binSup = await binanceSupported(t.symbol);
|
const binSup = await binanceSupported(t.symbol);
|
||||||
const okxSup = await okxSupported(t.symbol);
|
const okxSup = await okxSupported(t.symbol);
|
||||||
if (binSup) t.currentPrice = await getBinancePrice(t.symbol);
|
if (binSup) {
|
||||||
else if (okxSup) t.currentPrice = await getOkxPrice(t.symbol);
|
price = await getBinancePrice(t.symbol);
|
||||||
} catch {
|
} else if (okxSup) {
|
||||||
|
price = await getOkxPrice(t.symbol);
|
||||||
}
|
}
|
||||||
|
t.currentPrice = price;
|
||||||
|
} catch {}
|
||||||
result.push({
|
result.push({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
symbol: t.symbol,
|
symbol: t.symbol,
|
||||||
amount: t.amount,
|
amount: t.amount,
|
||||||
buyPrice: t.buyPrice,
|
buyPrice: t.buyPrice,
|
||||||
date: t.date,
|
date: t.date,
|
||||||
currentPrice: t.currentPrice,
|
currentPrice: t.currentPrice
|
||||||
note: t.note,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (changed) {
|
||||||
if (changed) writePortfolio(pf);
|
writePortfolio(pf);
|
||||||
|
}
|
||||||
res.json(result);
|
res.json(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -522,52 +415,6 @@ app.post("/api/portfolio/edit", (req, res) => {
|
|||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/api/trade_summary", (req, res) => {
|
|
||||||
const data = readTradeSummary();
|
|
||||||
data.trades.sort((a, b) => new Date(b.sellDate) - new Date(a.sellDate));
|
|
||||||
res.json(data.trades);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/api/portfolio/sell", (req, res) => {
|
|
||||||
const { id, sellAmount, sellPrice, sellDate } = req.body;
|
|
||||||
if (!id || isNaN(sellAmount) || isNaN(sellPrice) || !sellDate) {
|
|
||||||
return res.status(400).json({ error: "Ungültige Daten" });
|
|
||||||
}
|
|
||||||
const pf = readPortfolio();
|
|
||||||
let txIndex = pf.transactions.findIndex(x => String(x.id) === String(id));
|
|
||||||
if (txIndex === -1) {
|
|
||||||
return res.status(400).json({ error: "Transaktion nicht gefunden" });
|
|
||||||
}
|
|
||||||
let tx = pf.transactions[txIndex];
|
|
||||||
if (sellAmount > tx.amount) {
|
|
||||||
return res.status(400).json({ error: "Verkaufsmenge überschreitet die vorhandene Menge" });
|
|
||||||
}
|
|
||||||
const profit = (parseFloat(sellPrice) - parseFloat(tx.buyPrice)) * sellAmount;
|
|
||||||
const percentProfit = tx.buyPrice > 0 ? (profit / (tx.buyPrice * sellAmount)) * 100 : 0;
|
|
||||||
const tradeEntry = {
|
|
||||||
id: Date.now(),
|
|
||||||
symbol: tx.symbol,
|
|
||||||
amount: sellAmount,
|
|
||||||
buyPrice: tx.buyPrice,
|
|
||||||
sellPrice: parseFloat(sellPrice),
|
|
||||||
profit: profit,
|
|
||||||
percentProfit: percentProfit,
|
|
||||||
buyDate: tx.date,
|
|
||||||
sellDate: sellDate,
|
|
||||||
note: tx.note || ""
|
|
||||||
};
|
|
||||||
if (sellAmount < tx.amount) {
|
|
||||||
tx.amount = tx.amount - sellAmount;
|
|
||||||
} else {
|
|
||||||
pf.transactions.splice(txIndex, 1);
|
|
||||||
}
|
|
||||||
writePortfolio(pf);
|
|
||||||
const tsData = readTradeSummary();
|
|
||||||
tsData.trades.push(tradeEntry);
|
|
||||||
writeTradeSummary(tsData);
|
|
||||||
res.json({ success: true, trade: tradeEntry });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/api/portfolio/delete", (req, res) => {
|
app.post("/api/portfolio/delete", (req, res) => {
|
||||||
const { id } = req.body;
|
const { id } = req.body;
|
||||||
const pf = readPortfolio();
|
const pf = readPortfolio();
|
||||||
@ -627,61 +474,6 @@ app.get("/api/trade_summary", (req, res) => {
|
|||||||
|
|
||||||
app.use(express.static(path.join(__dirname, "..", "public")));
|
app.use(express.static(path.join(__dirname, "..", "public")));
|
||||||
|
|
||||||
function readTradeSummary() {
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(TRADE_SUMMARY_FILE, "utf8");
|
|
||||||
const data = JSON.parse(content);
|
|
||||||
|
|
||||||
if (data.trades) {
|
|
||||||
data.trades.forEach(trade => {
|
|
||||||
if (!trade.hasOwnProperty('note')) {
|
|
||||||
trade.note = "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error reading trade summary:", err);
|
|
||||||
return { trades: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeTradeSummary(data) {
|
|
||||||
try {
|
|
||||||
fs.writeFileSync(TRADE_SUMMARY_FILE, JSON.stringify(data, null, 2));
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error writing trade summary:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function readPortfolio() {
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(PORTFOLIO_FILE, "utf8");
|
|
||||||
const data = JSON.parse(content);
|
|
||||||
|
|
||||||
if (data.transactions) {
|
|
||||||
data.transactions.forEach(tx => {
|
|
||||||
if (!tx.hasOwnProperty('note')) {
|
|
||||||
tx.note = "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error reading portfolio:", err);
|
|
||||||
return { transactions: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function writePortfolio(data) {
|
|
||||||
try {
|
|
||||||
fs.writeFileSync(PORTFOLIO_FILE, JSON.stringify(data, null, 2));
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error writing portfolio:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
wss.on("connection", (ws) => {
|
wss.on("connection", (ws) => {
|
||||||
clients.add(ws);
|
clients.add(ws);
|
||||||
ws.on("close", () => {
|
ws.on("close", () => {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"version": "1.5.1",
|
"version": "1.5.0",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"Adding Note Option in Portfolio Page",
|
"Adding Portfolio Page",
|
||||||
"Fixing Editing Option in Trade Summary"
|
"Fixing Alarm Layout with Category",
|
||||||
|
"Automatically delete alarms that have expired"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user