mirror of
https://github.com/Gerald-Ha/HodlEye-Crypto-Price-Tracker.git
synced 2025-06-25 01:01:46 +00:00
Compare commits
2 Commits
214bb5c3d1
...
819f36f26f
Author | SHA1 | Date | |
---|---|---|---|
819f36f26f | |||
428408b659 |
19
docker-compose.yml
Normal file
19
docker-compose.yml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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,3 +205,26 @@ 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.0</span>
|
<span id="currentVersion">Version 1.5.1</span>
|
||||||
<span
|
<span
|
||||||
id="updateAvailable"
|
id="updateAvailable"
|
||||||
style="display: none; color: red; cursor: pointer"
|
style="display: none; color: red; cursor: pointer"
|
||||||
|
@ -1,24 +1,37 @@
|
|||||||
let portfolioData = [];
|
let portfolioData = [];
|
||||||
let coinColors = {};
|
let coinColors = {};
|
||||||
let addTransactionModal, editTransactionModal, colorConfigModal;
|
let addTransactionModal, editTransactionModal, colorConfigModal, noteModal;
|
||||||
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(() => {
|
||||||
@ -55,12 +68,14 @@ 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 = `
|
||||||
@ -76,9 +91,11 @@ 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;
|
||||||
@ -86,33 +103,40 @@ 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>${tx.buyPrice}</td>
|
<td>${formatPrice(tx.buyPrice)}</td>
|
||||||
<td>${tx.currentPrice.toFixed(2)}</td>
|
<td>${formatPrice(tx.currentPrice)}</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}</td>
|
<td>${tx.date}${editButton}${noteButton}</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>${(grouped[sym][0].currentPrice || 0).toFixed(2)}</td>
|
<td>Ø ${formatPrice(avgBuyPrice)}</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>
|
||||||
@ -147,6 +171,7 @@ 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) {
|
||||||
@ -155,12 +180,14 @@ 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]) {
|
||||||
@ -174,6 +201,7 @@ function drawPieChart() {
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
startAngle += sliceAngle;
|
startAngle += sliceAngle;
|
||||||
});
|
});
|
||||||
|
|
||||||
renderChartLegend(coinValues, totalAll);
|
renderChartLegend(coinValues, totalAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,6 +219,50 @@ 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();
|
||||||
@ -216,10 +288,12 @@ 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())
|
||||||
@ -257,18 +331,22 @@ 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) {
|
||||||
@ -278,6 +356,7 @@ function confirmSell() {
|
|||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedTransaction) {
|
if (!selectedTransaction) {
|
||||||
alert("Ungültig");
|
alert("Ungültig");
|
||||||
return;
|
return;
|
||||||
@ -314,6 +393,7 @@ 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");
|
||||||
@ -348,13 +428,16 @@ 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" },
|
||||||
@ -380,11 +463,25 @@ 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("Keine gültige Transaktion gefunden");
|
alert("No valid transaction found");
|
||||||
|
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" },
|
||||||
@ -396,11 +493,12 @@ function deleteTransaction() {
|
|||||||
if (j.error) {
|
if (j.error) {
|
||||||
alert(j.error);
|
alert(j.error);
|
||||||
} else {
|
} else {
|
||||||
|
closeDeleteConfirmationModal();
|
||||||
closeEditTransactionModal();
|
closeEditTransactionModal();
|
||||||
updatePrices();
|
updatePrices();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => alert("Fehler bei Delete"));
|
.catch(() => alert("Error deleting transaction"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function openColorConfigModal() {
|
function openColorConfigModal() {
|
||||||
@ -417,12 +515,14 @@ 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]) {
|
||||||
@ -467,3 +567,4 @@ 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; // Leitet zur Login-Seite um
|
window.location.href = response.url;
|
||||||
}
|
}
|
||||||
}).catch(error => console.error("Logout-Fehler:", error));
|
}).catch(error => console.error("Logout-Fehler:", error));
|
||||||
}
|
}
|
@ -1,6 +1,13 @@
|
|||||||
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -65,6 +72,9 @@ 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>
|
||||||
@ -74,7 +84,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}</td>
|
<td>${trade.sellDate}${editButton}${noteButton}</td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
|
|
||||||
@ -132,6 +142,142 @@ 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";
|
||||||
}
|
}
|
||||||
@ -177,4 +323,4 @@ function applyDateFilter() {
|
|||||||
renderTradeSummary(filtered);
|
renderTradeSummary(filtered);
|
||||||
updateBottomBar(filtered);
|
updateBottomBar(filtered);
|
||||||
closeDateFilterModal();
|
closeDateFilterModal();
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
const CURRENT_VERSION = "1.5.0";
|
const CURRENT_VERSION = "1.5.1";
|
||||||
|
|
||||||
function getUpdateUrl() {
|
function getUpdateUrl() {
|
||||||
return "/api/update?t=" + new Date().getTime();
|
return "/api/update?t=" + new Date().getTime();
|
||||||
|
@ -22,6 +22,17 @@
|
|||||||
.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>
|
||||||
@ -122,14 +133,24 @@
|
|||||||
<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>
|
|
||||||
<input type="text" id="editSymbol" disabled />
|
<div class="form-row">
|
||||||
<label for="editAmount">Amount:</label>
|
<label for="editSymbol">Symbol:</label>
|
||||||
<input type="number" step="0.000001" id="editAmount" />
|
<input type="text" id="editSymbol" disabled />
|
||||||
<label for="editBuyPrice">Buy Price (USDT):</label>
|
</div>
|
||||||
<input type="number" step="0.01" id="editBuyPrice" />
|
<div class="form-row">
|
||||||
<label for="editDate">Date:</label>
|
<label for="editAmount">Amount:</label>
|
||||||
<input type="date" id="editDate" />
|
<input type="number" step="0.000001" id="editAmount" />
|
||||||
|
</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>
|
||||||
@ -137,6 +158,17 @@
|
|||||||
</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>
|
||||||
@ -145,5 +177,16 @@
|
|||||||
<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,6 +7,7 @@
|
|||||||
<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%;
|
||||||
@ -21,6 +22,17 @@
|
|||||||
.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>
|
||||||
@ -32,8 +44,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>Trade Summary</button>
|
|
||||||
<button onclick="window.location.href='portfolio.html'">Add Transaction</button>
|
<button onclick="window.location.href='portfolio.html'">Add Transaction</button>
|
||||||
|
<button>Trade Summary</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>
|
||||||
@ -82,5 +94,59 @@
|
|||||||
</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>
|
||||||
|
61
server/alarms.js
Normal file
61
server/alarms.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
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.0
|
* Version: 1.5.1
|
||||||
* 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,6 +58,10 @@ 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();
|
||||||
@ -183,43 +187,97 @@ app.put("/api/cryptos", (req, res) => {
|
|||||||
res.json({ success: true, cryptos: data.cryptos });
|
res.json({ success: true, cryptos: data.cryptos });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/api/alarms", (req, res) => {
|
/* ------------- Notizen ------------- */
|
||||||
const data = readData();
|
|
||||||
res.json(data.alarms);
|
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/alarms", (req, res) => {
|
app.post("/api/trade_summary/note", (req, res) => {
|
||||||
const { symbol, price, frequency, direction } = req.body;
|
const { id, note } = req.body;
|
||||||
if (!symbol || !price) {
|
const ts = readTradeSummary();
|
||||||
return res.status(400).json({ error: "symbol and price are required." });
|
const trade = ts.trades.find((x) => String(x.id) === String(id));
|
||||||
}
|
if (!trade) return res.status(400).json({ error: "Invalid ID" });
|
||||||
const data = readData();
|
trade.note = note;
|
||||||
const newAlarm = {
|
writeTradeSummary(ts);
|
||||||
id: Date.now(),
|
res.json({ success: true });
|
||||||
symbol: symbol.toUpperCase(),
|
|
||||||
price: parseFloat(price),
|
|
||||||
frequency: frequency || "Once",
|
|
||||||
direction: direction || "Rising",
|
|
||||||
triggered: false
|
|
||||||
};
|
|
||||||
data.alarms.push(newAlarm);
|
|
||||||
writeData(data);
|
|
||||||
res.json(newAlarm);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.delete("/api/alarms/:id", (req, res) => {
|
const alarmsRouter = require("./alarms");
|
||||||
const alarmId = parseInt(req.params.id, 10);
|
app.use("/api/alarms", alarmsRouter);
|
||||||
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) {
|
||||||
@ -323,6 +381,53 @@ 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) {
|
||||||
@ -364,10 +469,12 @@ 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));
|
||||||
@ -375,29 +482,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) {
|
if (binSup) t.currentPrice = await getBinancePrice(t.symbol);
|
||||||
price = await getBinancePrice(t.symbol);
|
else if (okxSup) t.currentPrice = await getOkxPrice(t.symbol);
|
||||||
} else if (okxSup) {
|
} catch {
|
||||||
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) {
|
|
||||||
writePortfolio(pf);
|
if (changed) writePortfolio(pf);
|
||||||
}
|
|
||||||
res.json(result);
|
res.json(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -415,6 +522,52 @@ 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();
|
||||||
@ -474,6 +627,61 @@ 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.0",
|
"version": "1.5.1",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"Adding Portfolio Page",
|
"Adding Note Option in Portfolio Page",
|
||||||
"Fixing Alarm Layout with Category",
|
"Fixing Editing Option in Trade Summary"
|
||||||
"Automatically delete alarms that have expired"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user