trtr

Trading simulator and techanalysis gym
git clone https://git.ea.contact/trtr
Log | Files | Refs

commit 290f1f1670932964082754b7fbe1dadc68102afc
parent b0a575581ab922dfa106f724ff42995a32a902c6
Author: ea <ea@ea.contact>
Date:   Sun, 24 May 2026 13:46:13 +0000

minor changes

Diffstat:
A.gitignore | 1+
Mfrontend/index.html | 169++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/handlers.rs | 3++-
Msrc/session.rs | 4++--
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; }