commit 290f1f1670932964082754b7fbe1dadc68102afc
parent b0a575581ab922dfa106f724ff42995a32a902c6
Author: ea <ea@ea.contact>
Date: Sun, 24 May 2026 13:46:13 +0000
minor changes
Diffstat:
4 files changed, 173 insertions(+), 4 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/frontend/index.html b/frontend/index.html
@@ -156,6 +156,31 @@
cursor: pointer;
}
+ .fee-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ }
+
+ .fee-row label {
+ font-size: 0.72rem;
+ color: #808090;
+ white-space: nowrap;
+ }
+
+ .fee-input {
+ width: 68px;
+ background: #1e1e36;
+ border: 1px solid #3a3a5a;
+ border-radius: 4px;
+ color: #e0e0e0;
+ font-size: 0.82rem;
+ padding: 3px 6px;
+ text-align: right;
+ }
+ .fee-input:focus { outline: none; border-color: #f0b429; }
+
.btn {
width: 100%;
padding: 9px 0;
@@ -195,6 +220,73 @@
}
#next-btn:hover:not(:disabled) { background: #3a3a8a; color: #e0e0ff; }
#next-btn:disabled { opacity: 0.4; cursor: not-allowed; }
+
+ .trades-section {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ min-height: 0;
+ }
+
+ .trades-section h2 {
+ font-size: 0.7rem;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ color: #505070;
+ flex-shrink: 0;
+ }
+
+ #trades-list {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ overflow-y: auto;
+ max-height: 180px;
+ }
+
+ #trades-list:empty::after {
+ content: 'No trades yet';
+ font-size: 0.72rem;
+ color: #404060;
+ padding: 2px 0;
+ }
+
+ .trade-line-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: #1e1e36;
+ border-radius: 4px;
+ padding: 4px 6px;
+ }
+
+ .trade-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ }
+
+ .trade-price {
+ flex: 1;
+ font-size: 0.78rem;
+ font-variant-numeric: tabular-nums;
+ color: #c0c0d0;
+ }
+
+ .remove-btn {
+ background: none;
+ border: none;
+ color: #505070;
+ cursor: pointer;
+ font-size: 0.75rem;
+ padding: 1px 3px;
+ border-radius: 3px;
+ line-height: 1;
+ transition: color 0.1s;
+ flex-shrink: 0;
+ }
+ .remove-btn:hover { color: #ef5350; }
</style>
</head>
<body>
@@ -234,6 +326,15 @@
<div class="controls">
<h2>Trade</h2>
+ <div class="fee-row">
+ <label>Buy fee</label>
+ <span style="display:flex;align-items:center;gap:3px">
+ <input type="number" id="fee-input" class="fee-input"
+ value="0.10" min="0" max="100" step="0.01">
+ <span style="font-size:0.78rem;color:#606080">%</span>
+ </span>
+ </div>
+
<div class="trade-row">
<div class="slider-label">
<span>Buy %</span>
@@ -251,6 +352,14 @@
<input type="range" id="sell-pct" min="1" max="100" value="100">
<button class="btn btn-sell" id="sell-btn" disabled>SELL</button>
</div>
+
+ <div class="trades-section">
+ <div style="display:flex;align-items:center;justify-content:space-between">
+ <h2>Trade Lines</h2>
+ <button id="clear-lines-btn" class="remove-btn" style="font-size:0.68rem;padding:2px 6px">Clear</button>
+ </div>
+ <div id="trades-list"></div>
+ </div>
</div>
</div>
</div>
@@ -265,6 +374,61 @@
let candleSeries = null;
let currentPrice = 0;
+ // ── Trade lines ───────────────────────────────────────────────────────────
+
+ const tradeLines = [];
+ let tradeLineId = 0;
+
+ function addTradeLine(action, price) {
+ const color = action === 'buy' ? '#26a69a' : '#ef5350';
+ const priceLine = candleSeries.createPriceLine({
+ price,
+ color,
+ lineWidth: 1,
+ lineStyle: LightweightCharts.LineStyle.Dashed,
+ axisLabelVisible: true,
+ title: action.toUpperCase(),
+ });
+ tradeLines.push({ id: ++tradeLineId, action, price, priceLine });
+ renderTradeLines();
+ }
+
+ function removeTradeLine(id) {
+ const idx = tradeLines.findIndex(t => t.id === id);
+ if (idx === -1) return;
+ candleSeries.removePriceLine(tradeLines[idx].priceLine);
+ tradeLines.splice(idx, 1);
+ renderTradeLines();
+ }
+
+ function clearTradeLines() {
+ for (const t of tradeLines) candleSeries.removePriceLine(t.priceLine);
+ tradeLines.length = 0;
+ renderTradeLines();
+ }
+
+ function renderTradeLines() {
+ const list = document.getElementById('trades-list');
+ list.innerHTML = '';
+ for (const t of tradeLines) {
+ const color = t.action === 'buy' ? '#26a69a' : '#ef5350';
+ const item = document.createElement('div');
+ item.className = 'trade-line-item';
+ item.innerHTML =
+ `<span class="trade-dot" style="background:${color}"></span>` +
+ `<span class="trade-price">$${fmt(t.price)}</span>` +
+ `<button class="remove-btn" data-id="${t.id}">✕</button>`;
+ list.appendChild(item);
+ }
+ }
+
+ document.getElementById('trades-list').addEventListener('click', e => {
+ const btn = e.target.closest('.remove-btn');
+ if (btn) removeTradeLine(Number(btn.dataset.id));
+ });
+
+ document.getElementById('clear-lines-btn').addEventListener('click', clearTradeLines);
+
// ── Chart init ────────────────────────────────────────────────────────────
function initChart() {
@@ -310,6 +474,7 @@
async function startSession() {
setEnabled(false);
document.getElementById('loading-overlay').style.display = 'flex';
+ if (candleSeries) clearTradeLines();
try {
const res = await fetch('/api/session/new', { method: 'POST' });
@@ -377,15 +542,17 @@
async function trade(action) {
const pctId = action === 'buy' ? 'buy-pct' : 'sell-pct';
const pct = parseInt(document.getElementById(pctId).value, 10);
+ const fee_pct = parseFloat(document.getElementById('fee-input').value) || 0;
const res = await fetch('/api/trade', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ session_id: sessionId, action, pct }),
+ body: JSON.stringify({ session_id: sessionId, action, pct, fee_pct }),
});
const data = await res.json();
currentPrice = data.price;
updatePortfolio(data.usd, data.btc, data.pnl, data.price);
+ addTradeLine(action, data.price);
}
document.getElementById('buy-btn').addEventListener('click', () => trade('buy'));
diff --git a/src/handlers.rs b/src/handlers.rs
@@ -96,6 +96,7 @@ pub struct TradeBody {
session_id: String,
action: String,
pct: u8,
+ fee_pct: f64,
}
#[derive(Serialize)]
@@ -122,7 +123,7 @@ pub async fn trade(
let pct = body.pct as f64;
match body.action.as_str() {
- "buy" => session::execute_buy(&mut s, pct, price),
+ "buy" => session::execute_buy(&mut s, pct, price, body.fee_pct.clamp(0.0, 100.0)),
"sell" => session::execute_sell(&mut s, pct, price),
_ => {
drop(s);
diff --git a/src/session.rs b/src/session.rs
@@ -27,9 +27,9 @@ pub fn compute_pnl(state: &SessionState, current_price: f64) -> f64 {
(state.usd + state.btc * current_price) - state.initial_usd
}
-pub fn execute_buy(state: &mut SessionState, pct: f64, price: f64) {
+pub fn execute_buy(state: &mut SessionState, pct: f64, price: f64, fee_pct: f64) {
let spend = state.usd * (pct / 100.0);
- let btc_gained = spend / price;
+ let btc_gained = (spend / price) * (1.0 - fee_pct / 100.0);
state.usd -= spend;
state.btc += btc_gained;
}