| |
| |
| import { store, emitter, toast, sol as fsol } from './util.js'; |
| import { live, net } from './net.js'; |
| |
| const KEY = 'paper'; |
| const fresh = start => ({ start, sol: start, pos: {}, hist: [], realized: 0, created: Date.now() }); |
| |
| export const paper = Object.assign(emitter(), { |
| s: store.get(KEY, null) || fresh(10), |
| px: {}, |
| |
| save() { store.set(KEY, this.s); this.emit('change'); this.syncWatch(); }, |
| reset(start = this.s.start) { this.s = fresh(+start > 0 ? +start : 10); this.save(); }, |
| syncWatch() { net.watch(Object.keys(this.s.pos)); }, |
| |
| |
| learn(m, q) { if (q && q.rq > 0 && q.rt > 0) this.px[m] = { ...this.px[m], ...q }; }, |
| |
| quoteBuy(q, solIn) { |
| const solNet = solIn / (1 + q.fee); |
| const k = q.rq * q.rt; |
| return q.rt - k / (q.rq + solNet); |
| }, |
| quoteSell(q, tok) { |
| const k = q.rq * q.rt; |
| const gross = q.rq - k / (q.rt + tok); |
| return gross * (1 - q.fee); |
| }, |
| |
| buy(m, solIn, info = {}) { |
| const q = this.px[m]; |
| solIn = +solIn; |
| if (!q) return toast('No price for this coin yet', 'warn'); |
| if (!(solIn > 0)) return toast('Enter an amount', 'warn'); |
| if (solIn > this.s.sol + 1e-9) return toast(`Not enough SOL: ${fsol(this.s.sol)} in the paper wallet`, 'warn'); |
| const tok = this.quoteBuy(q, solIn); |
| if (!(tok > 0)) return toast('This pool cannot fill that', 'warn'); |
| const p = this.s.pos[m] ||= { tok: 0, cost: 0, spent: 0, got: 0, realized: 0, n: info.n || q.n || '', s: info.s || q.s || '', img: info.img || q.img || '', first: Date.now() }; |
| p.tok += tok; p.cost += solIn; p.spent += solIn; p.last = Date.now(); |
| if (info.n) p.n = info.n; if (info.s) p.s = info.s; if (info.img) p.img = info.img; |
| this.s.sol -= solIn; |
| this.trade(m, 1, solIn, tok, q, p); |
| toast(`Bought ${p.s || 'coin'} for ${fsol(solIn)} SOL`, 'buy', 2200); |
| return tok; |
| }, |
| |
| sell(m, frac) { |
| const p = this.s.pos[m]; |
| const q = this.px[m]; |
| if (!p || p.tok <= 0) return toast('Nothing to sell', 'warn'); |
| if (!q) return toast('No price for this coin yet', 'warn'); |
| frac = Math.min(1, Math.max(0, +frac)); |
| const tok = frac >= 0.999 ? p.tok : p.tok * frac; |
| const got = this.quoteSell(q, tok); |
| const costPart = p.cost * (tok / p.tok); |
| p.realized += got - costPart; this.s.realized += got - costPart; |
| p.cost -= costPart; p.tok -= tok; p.got += got; p.last = Date.now(); |
| this.s.sol += got; |
| this.trade(m, 0, got, tok, q, p); |
| if (p.tok <= 1e-6) { p.tok = 0; p.cost = 0; p.closed = Date.now(); } |
| toast(`Sold ${frac >= 0.999 ? 'all' : Math.round(frac * 100) + '%'} ${p.s || ''} for ${fsol(got)} SOL`, 'sell', 2200); |
| return got; |
| }, |
| |
| trade(m, buy, solAmt, tok, q, p) { |
| const k = q.rq * q.rt; |
| |
| if (buy) { q.rq += solAmt / (1 + q.fee); q.rt = k / q.rq; } else { q.rt += tok; q.rq = k / q.rt; } |
| const mc = q.mc || 0; |
| this.s.hist.unshift({ ts: Date.now(), m, n: p.n, s: p.s, img: p.img, buy, sol: solAmt, tok, mc }); |
| if (this.s.hist.length > 1000) this.s.hist.length = 1000; |
| this.save(); |
| this.emit('trade', { m, buy, sol: solAmt, tok, mc, ts: Date.now() }); |
| }, |
| |
| position(m) { return this.s.pos[m]; }, |
| value(m) { const p = this.s.pos[m], q = this.px[m]; return p && q ? p.tok * (q.price || q.rq / q.rt) : 0; }, |
| |
| totals() { |
| let value = 0, cost = 0; |
| for (const [m, p] of Object.entries(this.s.pos)) { if (p.tok > 0) { value += this.value(m); cost += p.cost; } } |
| return { sol: this.s.sol, value, cost, unreal: value - cost, realized: this.s.realized, total: this.s.sol + value, start: this.s.start }; |
| }, |
| }); |
| |
| live.on('px', m => { |
| for (const [mint, a] of Object.entries(m.p)) { |
| const [price, mc, pool, rq, rt, fee, n, s, img] = a; |
| paper.px[mint] = { ...paper.px[mint], price, mc, pool, rq, rt, fee, n, s, img }; |
| } |
| paper.emit('prices'); |
| }); |
| |
| paper.syncWatch(); |