yns?&2GN^(X)9_z5Eze0tID;?94hF06+a
zZSZ4%?%$06i;>_Z4@V}|xL(!;Y02}_j|M}CO89oDl7D9Mz
S?2GvC+kbX!?Cxht%Kjg+gUitX
diff --git a/new/portfolio.py b/new/portfolio.py
deleted file mode 100644
index 32b711e..0000000
--- a/new/portfolio.py
+++ /dev/null
@@ -1,333 +0,0 @@
-# portfolio.py
-from __future__ import annotations
-import math, time, json
-from typing import Dict, List, Any, Optional
-
-# ========= TRYB KSIĘGOWOŚCI =========
-# "stock" – akcje (short dodaje gotówkę przy otwarciu, przy zamknięciu oddajesz notional)
-# "margin" – FX/CFD (short NIE zmienia cash przy otwarciu; cash zmienia się o zrealizowany PnL przy zamknięciu)
-ACCOUNTING_MODE = "margin"
-
-# ========= RYZYKO / ATR =========
-USE_ATR = True # jeśli sygnał ma "atr" > 0, to SL/TP liczone od ATR
-SL_ATR_MULT = 2.0 # SL = 2.0 * ATR
-TP_ATR_MULT = 3.0 # TP = 3.0 * ATR
-RISK_FRACTION = 0.003 # 0.3% equity na trade (position sizing wg 1R)
-
-# fallback procentowy, gdy brak ATR
-SL_PCT = 0.010
-TP_PCT = 0.020
-TRAIL_PCT = 0.020 # spokojniejszy trailing
-
-SYMBOL_OVERRIDES = {
- # "EURUSD=X": {"SL_PCT": 0.0025, "TP_PCT": 0.0050, "TRAIL_PCT": 0.0030},
-}
-
-# ========= LIMITY BUDŻETOWE =========
-ALLOC_FRACTION = 0.25 # max % gotówki na JEDEN LONG
-MIN_TRADE_CASH = 25.0
-MAX_NEW_POSITIONS_PER_CYCLE = 2 # None aby wyłączyć limit
-
-# =================================
-def _to_native(obj: Any):
- """JSON-safe: NaN/Inf -> None; rekurencyjnie czyści struktury."""
- if isinstance(obj, float):
- return None if (math.isnan(obj) or math.isinf(obj)) else obj
- if isinstance(obj, (int, str)) or obj is None:
- return obj
- if isinstance(obj, dict):
- return {k: _to_native(v) for k, v in obj.items()}
- if isinstance(obj, (list, tuple)):
- return [_to_native(x) for x in obj]
- try:
- return float(obj)
- except Exception:
- return str(obj)
-
-def save_json(path: str, data: Any) -> None:
- with open(path, "w", encoding="utf-8") as f:
- json.dump(_to_native(data), f, ensure_ascii=False, indent=2, allow_nan=False)
-
-class Portfolio:
- """
- LONG open: cash -= qty*price ; close: cash += qty*price ; PnL = (px - entry)*qty
- SHORT open (stock): cash += qty*price ; close: cash -= qty*price ; PnL = (entry - px)*qty
- SHORT open (margin): cash bez zmian ; close: cash += PnL ; PnL = (entry - px)*qty
-
- total_value:
- - stock -> cash + positions_net (wartość likwidacyjna)
- - margin -> start_capital + realized_pnl + unrealized_pnl
- """
- def __init__(self, capital: float):
- self.cash = float(capital)
- self.start_capital = float(capital)
- self.positions: Dict[str, Dict] = {}
- self.history: List[Dict] = []
- self.trade_log: List[Dict] = []
- self.realized_pnl: float = 0.0
- self.last_prices: Dict[str, float] = {} # ostatnie znane ceny
-
- # ---------- helpers ----------
- def _get_params(self, ticker: str):
- o = SYMBOL_OVERRIDES.get(ticker, {})
- sl = float(o.get("SL_PCT", SL_PCT))
- tp = float(o.get("TP_PCT", TP_PCT))
- tr = float(o.get("TRAIL_PCT", TRAIL_PCT))
- return sl, tp, tr
-
- def _init_risk(self, ticker: str, entry: float, side: int, atr: Optional[float] = None):
- """Zwraca: stop, take, trail_best, trail_stop, one_r."""
- sl_pct, tp_pct, trail_pct = self._get_params(ticker)
- use_atr = USE_ATR and atr is not None and isinstance(atr, (int, float)) and atr > 0.0
-
- if use_atr:
- sl_dist = SL_ATR_MULT * float(atr)
- tp_dist = TP_ATR_MULT * float(atr)
- stop = entry - sl_dist if side == 1 else entry + sl_dist
- take = entry + tp_dist if side == 1 else entry - tp_dist
- one_r = sl_dist
- else:
- if side == 1:
- stop = entry * (1.0 - sl_pct); take = entry * (1.0 + tp_pct)
- else:
- stop = entry * (1.0 + sl_pct); take = entry * (1.0 - tp_pct)
- one_r = abs(entry - stop)
-
- trail_best = entry
- trail_stop = stop
- return stop, take, trail_best, trail_stop, max(one_r, 1e-8)
-
- def _update_trailing(self, ticker: str, pos: Dict, price: float):
- """Breakeven po 1R + trailing procentowy."""
- _, _, trail_pct = self._get_params(ticker)
-
- # BE po 1R
- if pos["side"] == 1 and not pos.get("be_done", False):
- if price >= pos["entry"] + pos["one_r"]:
- pos["stop"] = max(pos["stop"], pos["entry"])
- pos["be_done"] = True
- elif pos["side"] == -1 and not pos.get("be_done", False):
- if price <= pos["entry"] - pos["one_r"]:
- pos["stop"] = min(pos["stop"], pos["entry"])
- pos["be_done"] = True
-
- # trailing
- if pos["side"] == 1:
- if price > pos["trail_best"]:
- pos["trail_best"] = price
- pos["trail_stop"] = pos["trail_best"] * (1.0 - trail_pct)
- else:
- if price < pos["trail_best"]:
- pos["trail_best"] = price
- pos["trail_stop"] = pos["trail_best"] * (1.0 + trail_pct)
-
- def _positions_values(self, prices: Optional[Dict[str, float]] = None) -> Dict[str, float]:
- """Zwraca Σ|qty*px| oraz Σqty*px*side. Fallback ceny: prices -> self.last_prices -> entry."""
- gross = 0.0
- net = 0.0
- for t, p in self.positions.items():
- px = None
- if prices is not None:
- px = prices.get(t)
- if px is None or (isinstance(px, float) and math.isnan(px)):
- px = self.last_prices.get(t)
- if px is None or (isinstance(px, float) and math.isnan(px)):
- px = p["entry"]
- gross += abs(p["qty"] * px)
- net += p["qty"] * px * p["side"]
- return {"positions_gross": gross, "positions_net": net}
-
- def unrealized_pnl(self, prices: Dict[str, float]) -> float:
- """Σ side * (px - entry) * qty."""
- upnl = 0.0
- for t, p in self.positions.items():
- px = prices.get(t, self.last_prices.get(t, p["entry"]))
- if px is None or (isinstance(px, float) and math.isnan(px)): px = p["entry"]
- upnl += p["side"] * (px - p["entry"]) * p["qty"]
- return upnl
-
- def equity_from_start(self, prices: Dict[str, float]) -> float:
- return self.start_capital + self.realized_pnl + self.unrealized_pnl(prices)
-
- # ---------- zamknięcia / log ----------
- def _close_position(self, ticker: str, price: float, reason: str):
- pos = self.positions.get(ticker)
- if not pos: return
- qty, entry, side = pos["qty"], pos["entry"], pos["side"]
-
- if side == 1:
- self.cash += qty * price
- pnl_abs = (price - entry) * qty
- else:
- pnl_abs = (entry - price) * qty
- if ACCOUNTING_MODE == "stock":
- self.cash -= qty * price
- else:
- self.cash += pnl_abs # margin: tylko PnL wpływa na cash
-
- denom = max(qty * entry, 1e-12)
- pnl_pct = pnl_abs / denom
- self.realized_pnl += pnl_abs
-
- self._log_trade("SELL", ticker, price, qty, side, pnl_abs, pnl_pct, reason=reason)
- del self.positions[ticker]
-
- def _log_trade(self, action: str, ticker: str, price: float, qty: float, side: int,
- pnl_abs: float = 0.0, pnl_pct: float = 0.0, reason: Optional[str] = None):
- ts = time.strftime("%Y-%m-%d %H:%M:%S")
- if action == "BUY":
- print(f"[{ts}] BUY {ticker} @ {price:.6f}, qty={qty:.6f}, side={'LONG' if side==1 else 'SHORT'}, cash={self.cash:.2f}")
- else:
- r = f", reason={reason}" if reason else ""
- print(f"[{ts}] SELL {ticker} @ {price:.6f}, qty={qty:.6f}, PnL={pnl_abs:+.2f} ({pnl_pct:+.2%}), cash={self.cash:.2f}{r}")
- self.trade_log.append({
- "time": ts, "action": action, "ticker": ticker, "price": float(price),
- "qty": float(qty), "side": int(side),
- "pnl_abs": float(pnl_abs), "pnl_pct": float(pnl_pct),
- "realized_pnl_cum": float(self.realized_pnl), "cash_after": float(self.cash),
- "reason": reason or ""
- })
-
- # ---------- główna aktualizacja ----------
- def on_signals(self, sigs: List[dict]):
- # normalizacja
- clean: List[Dict[str, Any]] = []
- for s in sigs:
- if isinstance(s, dict):
- px = s.get("price")
- if s.get("error") is None and px is not None and not math.isnan(px):
- clean.append(s)
- else:
- if getattr(s, "error", None) is None and not math.isnan(getattr(s, "price", float("nan"))):
- clean.append({
- "ticker": s.ticker, "price": s.price, "signal": s.signal, "time": s.time,
- "atr": getattr(s, "atr", None)
- })
-
- # snapshot, gdy brak świeżych danych
- if not clean:
- vals = self._positions_values(None)
- prices_map = {t: self.last_prices.get(t, p["entry"]) for t, p in self.positions.items()}
- if ACCOUNTING_MODE == "stock":
- account_value = self.cash + vals["positions_net"]
- else:
- account_value = self.equity_from_start(prices_map)
- unreal = self.unrealized_pnl(prices_map)
- self.history.append({
- "time": time.strftime("%Y-%m-%d %H:%M:%S"),
- "cash": float(self.cash),
- "positions_value": float(vals["positions_gross"]),
- "positions_net": float(vals["positions_net"]),
- "total_value": float(account_value),
- "equity_from_start": float(self.start_capital + self.realized_pnl + unreal),
- "unrealized_pnl": float(unreal),
- "realized_pnl_cum": float(self.realized_pnl),
- "open_positions": int(len(self.positions))
- })
- save_json("portfolio_history.json", self.history)
- return
-
- # mapy cen
- prices = {s["ticker"]: float(s["price"]) for s in clean}
- self.last_prices.update(prices)
-
- # 1) risk exits (SL/TP/TRAIL + BE)
- to_close = []
- for t, pos in list(self.positions.items()):
- price = prices.get(t)
- if price is None or math.isnan(price): continue
- self._update_trailing(t, pos, price)
- if pos["side"] == 1:
- if price <= pos["stop"]: to_close.append((t, "SL"))
- elif price >= pos["take"]: to_close.append((t, "TP"))
- elif price <= pos.get("trail_stop", -float("inf")): to_close.append((t, "TRAIL"))
- else:
- if price >= pos["stop"]: to_close.append((t, "SL"))
- elif price <= pos["take"]: to_close.append((t, "TP"))
- elif price >= pos.get("trail_stop", float("inf")): to_close.append((t, "TRAIL"))
- for t, reason in to_close:
- price = prices.get(t)
- if price is not None and not math.isnan(price) and t in self.positions:
- self._close_position(t, price, reason=reason)
-
- # 2) zamknięcie na przeciwny/HOLD
- for s in clean:
- t, sig = s["ticker"], int(s["signal"])
- price = float(s["price"])
- if t in self.positions:
- pos = self.positions[t]
- if sig == 0 or sig != pos["side"]:
- self._close_position(t, price, reason="SIGNAL")
-
- # 3) otwarcia – ATR sizing + limity
- candidates = [s for s in clean if s["ticker"] not in self.positions and int(s["signal"]) != 0]
- if MAX_NEW_POSITIONS_PER_CYCLE and MAX_NEW_POSITIONS_PER_CYCLE > 0:
- candidates = candidates[:MAX_NEW_POSITIONS_PER_CYCLE]
-
- for s in candidates:
- t, sig, price = s["ticker"], int(s["signal"]), float(s["price"])
- atr = s.get("atr", None)
- if price <= 0: continue
-
- stop, take, trail_best, trail_stop, one_r = self._init_risk(t, price, sig, atr)
-
- # equity teraz (dla sizingu ryzyka)
- if ACCOUNTING_MODE == "margin":
- equity_now = self.start_capital + self.realized_pnl + self.unrealized_pnl(self.last_prices)
- else:
- vals_tmp = self._positions_values(self.last_prices)
- equity_now = self.cash + vals_tmp["positions_net"]
-
- risk_amount = max(1e-6, equity_now * RISK_FRACTION)
- qty_risk = risk_amount / max(one_r, 1e-8)
-
- # limit gotówki dla LONG
- per_trade_cash = max(MIN_TRADE_CASH, self.cash * ALLOC_FRACTION)
- qty_cash = per_trade_cash / max(price, 1e-12) if sig == 1 else float("inf")
-
- qty = max(0.0, min(qty_risk, qty_cash))
- if qty <= 0: continue
-
- # księgowanie gotówki przy otwarciu
- if sig == 1:
- cost = qty * price
- if self.cash < cost: continue
- self.cash -= cost
- else:
- if ACCOUNTING_MODE == "stock":
- self.cash += qty * price
- # margin: brak zmiany cash przy short open
-
- self.positions[t] = {
- "qty": qty, "entry": price, "side": sig,
- "stop": stop, "take": take,
- "trail_best": trail_best, "trail_stop": trail_stop,
- "one_r": one_r, "be_done": False
- }
- self._log_trade("BUY" if sig == 1 else "SELL", t, price, qty, sig)
-
- # 4) snapshot na bieżących/ostatnich cenach
- vals = self._positions_values(self.last_prices)
- prices_map = {t: self.last_prices.get(t, p["entry"]) for t, p in self.positions.items()}
- if ACCOUNTING_MODE == "stock":
- account_value = self.cash + vals["positions_net"]
- else:
- account_value = self.equity_from_start(prices_map)
- unreal = self.unrealized_pnl(prices_map)
-
- self.history.append({
- "time": clean[0]["time"],
- "cash": float(self.cash),
- "positions_value": float(vals["positions_gross"]),
- "positions_net": float(vals["positions_net"]),
- "total_value": float(account_value),
- "equity_from_start": float(self.start_capital + self.realized_pnl + unreal),
- "unrealized_pnl": float(unreal),
- "realized_pnl_cum": float(self.realized_pnl),
- "open_positions": int(len(self.positions))
- })
-
- # 5) zapisy
- save_json("trade_log.json", self.trade_log)
- save_json("portfolio_history.json", self.history)
- save_json("positions.json", [{"ticker": t, **p} for t, p in self.positions.items()])
diff --git a/new/portfolio_history.json b/new/portfolio_history.json
deleted file mode 100644
index 57abe6e..0000000
--- a/new/portfolio_history.json
+++ /dev/null
@@ -1,79 +0,0 @@
-[
- {
- "time": "2025-08-15 10:11:00+00:00",
- "cash": 10000.0,
- "positions_value": 0.0,
- "positions_net": 0.0,
- "total_value": 10000.0,
- "equity_from_start": 10000.0,
- "unrealized_pnl": 0.0,
- "realized_pnl_cum": 0.0,
- "open_positions": 0
- },
- {
- "time": "2025-08-15 10:12:00+00:00",
- "cash": 10000.0,
- "positions_value": 0.0,
- "positions_net": 0.0,
- "total_value": 10000.0,
- "equity_from_start": 10000.0,
- "unrealized_pnl": 0.0,
- "realized_pnl_cum": 0.0,
- "open_positions": 0
- },
- {
- "time": "2025-08-15 10:13:00+00:00",
- "cash": 10000.0,
- "positions_value": 0.0,
- "positions_net": 0.0,
- "total_value": 10000.0,
- "equity_from_start": 10000.0,
- "unrealized_pnl": 0.0,
- "realized_pnl_cum": 0.0,
- "open_positions": 0
- },
- {
- "time": "2025-08-15 10:14:00+00:00",
- "cash": 10000.0,
- "positions_value": 0.0,
- "positions_net": 0.0,
- "total_value": 10000.0,
- "equity_from_start": 10000.0,
- "unrealized_pnl": 0.0,
- "realized_pnl_cum": 0.0,
- "open_positions": 0
- },
- {
- "time": "2025-08-15 10:15:00+00:00",
- "cash": 10000.0,
- "positions_value": 0.0,
- "positions_net": 0.0,
- "total_value": 10000.0,
- "equity_from_start": 10000.0,
- "unrealized_pnl": 0.0,
- "realized_pnl_cum": 0.0,
- "open_positions": 0
- },
- {
- "time": "2025-08-15 10:16:00+00:00",
- "cash": 10000.0,
- "positions_value": 5999.999999999975,
- "positions_net": -5999.999999999975,
- "total_value": 10000.0,
- "equity_from_start": 10000.0,
- "unrealized_pnl": 0.0,
- "realized_pnl_cum": 0.0,
- "open_positions": 2
- },
- {
- "time": "2025-08-15 10:17:00+00:00",
- "cash": 10000.0,
- "positions_value": 5999.999999999975,
- "positions_net": -5999.999999999975,
- "total_value": 10000.0,
- "equity_from_start": 10000.0,
- "unrealized_pnl": 0.0,
- "realized_pnl_cum": 0.0,
- "open_positions": 2
- }
-]
\ No newline at end of file
diff --git a/new/positions.json b/new/positions.json
deleted file mode 100644
index 01abac4..0000000
--- a/new/positions.json
+++ /dev/null
@@ -1,26 +0,0 @@
-[
- {
- "ticker": "OP-USD",
- "qty": 3973.896468582607,
- "entry": 0.7549265623092651,
- "side": -1,
- "stop": 0.7624758279323578,
- "take": 0.7398280310630798,
- "trail_best": 0.7549265623092651,
- "trail_stop": 0.7624758279323578,
- "one_r": 0.00754926562309266,
- "be_done": false
- },
- {
- "ticker": "NEAR-USD",
- "qty": 1076.5240904642392,
- "entry": 2.7867467403411865,
- "side": -1,
- "stop": 2.8146142077445986,
- "take": 2.731011805534363,
- "trail_best": 2.7867467403411865,
- "trail_stop": 2.8146142077445986,
- "one_r": 0.02786746740341206,
- "be_done": false
- }
-]
\ No newline at end of file
diff --git a/new/static/app.js b/new/static/app.js
deleted file mode 100644
index 26fc734..0000000
--- a/new/static/app.js
+++ /dev/null
@@ -1,221 +0,0 @@
-// static/app.js
-const fmt2 = (x) => (x == null || isNaN(x) ? "—" : Number(x).toFixed(2));
-const fmt6 = (x) => (x == null || isNaN(x) ? "—" : Number(x).toFixed(6));
-const fmtPct= (x) => (x == null || isNaN(x) ? "—" : (Number(x) * 100).toFixed(2) + "%");
-
-async function getJSON(url) {
- const r = await fetch(url, { cache: "no-store" });
- if (!r.ok) throw new Error(`${url}: ${r.status}`);
- return r.json();
-}
-
-// prosty wykres linii na canvas (bez bibliotek)
-function drawLineChart(canvas, points) {
- const ctx = canvas.getContext("2d");
- const pad = 32;
- const w = canvas.width, h = canvas.height;
- ctx.clearRect(0, 0, w, h);
-
- if (!points || points.length === 0) {
- ctx.fillStyle = "#9aa3b2";
- ctx.fillText("Brak danych", 10, 20);
- return;
- }
-
- const n = points.length;
- const ys = points.map(p => p.y);
- const minY = Math.min(...ys), maxY = Math.max(...ys);
- const yLo = minY === maxY ? minY - 1 : minY;
- const yHi = minY === maxY ? maxY + 1 : maxY;
-
- const x0 = pad, y0 = h - pad, x1 = w - pad, y1 = pad;
- const xScale = (i) => x0 + (i / (n - 1)) * (x1 - x0);
- const yScale = (y) => y0 - ((y - yLo) / (yHi - yLo)) * (y0 - y1);
-
- // osie
- ctx.strokeStyle = "#242a36";
- ctx.lineWidth = 1;
- ctx.beginPath();
- ctx.moveTo(x0, y0); ctx.lineTo(x1, y0);
- ctx.moveTo(x0, y0); ctx.lineTo(x0, y1);
- ctx.stroke();
-
- // siatka
- ctx.strokeStyle = "#1b2130";
- [0.25, 0.5, 0.75].forEach(f => {
- const yy = y0 - (y0 - y1) * f;
- ctx.beginPath();
- ctx.moveTo(x0, yy); ctx.lineTo(x1, yy);
- ctx.stroke();
- });
-
- // podpisy min/max
- ctx.fillStyle = "#9aa3b2";
- ctx.font = "12px system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial";
- ctx.fillText(fmt2(yHi), 6, y1 + 10);
- ctx.fillText(fmt2(yLo), 6, y0 - 2);
-
- // linia
- ctx.strokeStyle = "#4da3ff";
- ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.moveTo(xScale(0), yScale(ys[0]));
- for (let i = 1; i < n; i++) ctx.lineTo(xScale(i), yScale(ys[i]));
- ctx.stroke();
-
- // kropka na końcu
- const lastX = xScale(n - 1), lastY = yScale(ys[n - 1]);
- ctx.fillStyle = "#e7e9ee";
- ctx.beginPath(); ctx.arc(lastX, lastY, 3, 0, Math.PI * 2); ctx.fill();
- ctx.fillText(fmt2(ys[n - 1]), lastX + 6, lastY - 6);
-}
-
-async function loadAll() {
- try {
- const [snap, hist, pos, trades] = await Promise.all([
- getJSON("/api/snapshot"),
- getJSON("/api/history"),
- getJSON("/api/positions"),
- getJSON("/api/trades"),
- ]);
-
- // czas
- document.getElementById("last-update").textContent =
- "Ostatnia aktualizacja: " + (snap?.last_history?.time || "—");
-
- // ===== ostatni wiersz historii =====
- const last = Array.isArray(hist) && hist.length ? hist[hist.length - 1] : null;
-
- const cashVal = Number(last?.cash ?? 0);
- const totalVal = Number(
- last?.total_value ??
- (Number(last?.cash ?? 0) + Number(last?.positions_net ?? 0)) // fallback dla starszych logów
- );
-
- // KARTY
- document.getElementById("cash").textContent = fmt2(cashVal);
- document.getElementById("total-value").textContent = fmt2(totalVal);
-
- // mapa ostatnich cen do liczenia zysku (unrealized)
- const lastPrice = new Map();
- (snap?.signals || []).forEach(s => {
- const px = Number(s.price);
- if (!isNaN(px)) lastPrice.set(s.ticker, px);
- });
-
- // Zysk = suma niezrealizowanych PnL na otwartych pozycjach
- let unrealPnL = 0;
- (pos || []).forEach(p => {
- const price = Number(lastPrice.get(p.ticker));
- const entry = Number(p.entry);
- const qty = Number(p.qty);
- const side = Number(p.side);
- if (isNaN(price) || isNaN(entry) || isNaN(qty) || isNaN(side)) return;
- unrealPnL += side === 1 ? (price - entry) * qty : (entry - price) * qty;
- });
- const unrlEl = document.getElementById("unrealized");
- unrlEl.textContent = fmt2(unrealPnL);
- unrlEl.classList.remove("pnl-positive", "pnl-negative");
- if (unrealPnL > 0) unrlEl.classList.add("pnl-positive");
- else if (unrealPnL < 0) unrlEl.classList.add("pnl-negative");
-
- // liczba otwartych pozycji
- document.getElementById("open-pos").textContent =
- Number(last?.open_positions ?? (pos?.length ?? 0));
-
- // ===== WYKRES WARTOŚCI KONTA (TOTAL) =====
- const totalCanvas = document.getElementById("totalChart");
- const totalPoints = (hist || [])
- .map((row, i) => {
- const v = (row.total_value != null)
- ? Number(row.total_value)
- : Number(row.cash ?? 0) + Number(row.positions_net ?? 0);
- return { x: i, y: v };
- })
- .filter(p => !isNaN(p.y))
- .slice(-500);
- drawLineChart(totalCanvas, totalPoints);
-
- // ===== WYKRES GOTÓWKI (CASH) =====
- const cashCanvas = document.getElementById("cashChart");
- const cashPoints = (hist || [])
- .map((row, i) => ({ x: i, y: Number(row.cash ?? NaN) }))
- .filter(p => !isNaN(p.y))
- .slice(-500);
- drawLineChart(cashCanvas, cashPoints);
-
- // ===== POZYCJE =====
- const posBody = document.querySelector("#positions-table tbody");
- posBody.innerHTML = "";
- (pos || []).forEach((p) => {
- const price = Number(lastPrice.get(p.ticker));
- const entry = Number(p.entry);
- const qty = Number(p.qty);
- const side = Number(p.side);
-
- let upnl = NaN, upct = NaN;
- if (!isNaN(price) && !isNaN(entry) && !isNaN(qty) && !isNaN(side)) {
- upnl = side === 1 ? (price - entry) * qty : (entry - price) * qty;
- const denom = Math.max(qty * entry, 1e-12);
- upct = upnl / denom;
- }
- const pnlClass = upnl > 0 ? "pnl-positive" : (upnl < 0 ? "pnl-negative" : "");
-
- const tr = document.createElement("tr");
- tr.innerHTML = `
-
${p.ticker} |
- ${fmt6(qty)} |
- ${fmt6(entry)} |
- ${side === 1 ? "LONG" : "SHORT"} |
- ${fmt6(price)} |
- ${fmt2(upnl)} |
- ${fmtPct(upct)} |
- `;
- posBody.appendChild(tr);
- });
-
- // ===== SYGNAŁY =====
- const sigBody = document.querySelector("#signals-table tbody");
- sigBody.innerHTML = "";
- const signals = (snap?.signals || []).slice().sort((a,b)=>a.ticker.localeCompare(b.ticker));
- signals.forEach((s) => {
- const sigTxt = s.signal === 1 ? "BUY" : (s.signal === -1 ? "SELL" : "HOLD");
- const tr = document.createElement("tr");
- tr.innerHTML = `
- ${s.ticker} |
- ${s.time} |
- ${fmt6(s.price)} |
- ${sigTxt} |
- ${s.interval} |
- `;
- sigBody.appendChild(tr);
- });
-
- // ===== TRANSAKCJE =====
- const tradesBody = document.querySelector("#trades-table tbody");
- tradesBody.innerHTML = "";
- (trades || []).slice(-50).reverse().forEach((t) => {
- const pnlClass = t.pnl_abs > 0 ? "pnl-positive" : (t.pnl_abs < 0 ? "pnl-negative" : "");
- const tr = document.createElement("tr");
- tr.innerHTML = `
- ${t.time} |
- ${t.action} |
- ${t.ticker} |
- ${fmt6(t.price)} |
- ${fmt6(t.qty)} |
- ${fmt2(t.pnl_abs)} |
- ${fmtPct(t.pnl_pct)} |
- ${fmt2(t.cash_after)} |
- `;
- tradesBody.appendChild(tr);
- });
-
- } catch (e) {
- console.error("loadAll error:", e);
- document.getElementById("last-update").textContent = "Błąd ładowania danych";
- }
-}
-
-document.getElementById("refresh-btn").addEventListener("click", loadAll);
-loadAll();
-setInterval(loadAll, 1000);
diff --git a/new/templates/index.html b/new/templates/index.html
deleted file mode 100644
index da2a5a9..0000000
--- a/new/templates/index.html
+++ /dev/null
@@ -1,114 +0,0 @@
-
-
-
-
-
- Trading Dashboard
-
-
-
-
-
-
-
-
-
-
-
Wartość konta (total)
-
—
-
-
-
Zysk (otwarte pozycje)
-
—
-
-
-
-
-
-
- Wartość konta (total) — wykres
-
-
-
-
-
-
-
- Gotówka — wykres
-
-
-
-
-
-
-
- Otwarte pozycje
-
-
-
- | Ticker |
- Qty |
- Entry |
- Side |
- Last price |
- Unreal. PnL |
- Unreal. PnL % |
-
-
-
-
-
-
-
-
- Ostatnie sygnały
-
-
-
- | Ticker |
- Time |
- Price |
- Signal |
- Interval |
-
-
-
-
-
-
-
-
- Transakcje (ostatnie 50)
-
-
-
- | Time |
- Action |
- Ticker |
- Price |
- Qty |
- PnL |
- PnL % |
- Cash po |
-
-
-
-
-
-
-
-
-
-
diff --git a/new/trade_log.json b/new/trade_log.json
deleted file mode 100644
index 2a1d04a..0000000
--- a/new/trade_log.json
+++ /dev/null
@@ -1,28 +0,0 @@
-[
- {
- "time": "2025-08-15 12:18:01",
- "action": "SELL",
- "ticker": "OP-USD",
- "price": 0.7549265623092651,
- "qty": 3973.896468582607,
- "side": -1,
- "pnl_abs": 0.0,
- "pnl_pct": 0.0,
- "realized_pnl_cum": 0.0,
- "cash_after": 10000.0,
- "reason": ""
- },
- {
- "time": "2025-08-15 12:18:01",
- "action": "SELL",
- "ticker": "NEAR-USD",
- "price": 2.7867467403411865,
- "qty": 1076.5240904642392,
- "side": -1,
- "pnl_abs": 0.0,
- "pnl_pct": 0.0,
- "realized_pnl_cum": 0.0,
- "cash_after": 10000.0,
- "reason": ""
- }
-]
\ No newline at end of file
diff --git a/portfolio.py b/portfolio.py
index cee0213..32b711e 100644
--- a/portfolio.py
+++ b/portfolio.py
@@ -1,11 +1,39 @@
# portfolio.py
from __future__ import annotations
import math, time, json
-from typing import Dict, List, Any
+from typing import Dict, List, Any, Optional
+# ========= TRYB KSIĘGOWOŚCI =========
+# "stock" – akcje (short dodaje gotówkę przy otwarciu, przy zamknięciu oddajesz notional)
+# "margin" – FX/CFD (short NIE zmienia cash przy otwarciu; cash zmienia się o zrealizowany PnL przy zamknięciu)
+ACCOUNTING_MODE = "margin"
+
+# ========= RYZYKO / ATR =========
+USE_ATR = True # jeśli sygnał ma "atr" > 0, to SL/TP liczone od ATR
+SL_ATR_MULT = 2.0 # SL = 2.0 * ATR
+TP_ATR_MULT = 3.0 # TP = 3.0 * ATR
+RISK_FRACTION = 0.003 # 0.3% equity na trade (position sizing wg 1R)
+
+# fallback procentowy, gdy brak ATR
+SL_PCT = 0.010
+TP_PCT = 0.020
+TRAIL_PCT = 0.020 # spokojniejszy trailing
+
+SYMBOL_OVERRIDES = {
+ # "EURUSD=X": {"SL_PCT": 0.0025, "TP_PCT": 0.0050, "TRAIL_PCT": 0.0030},
+}
+
+# ========= LIMITY BUDŻETOWE =========
+ALLOC_FRACTION = 0.25 # max % gotówki na JEDEN LONG
+MIN_TRADE_CASH = 25.0
+MAX_NEW_POSITIONS_PER_CYCLE = 2 # None aby wyłączyć limit
+
+# =================================
def _to_native(obj: Any):
- """Bezpieczne rzutowanie do typów akceptowalnych przez JSON."""
- if isinstance(obj, (float, int, str)) or obj is None:
+ """JSON-safe: NaN/Inf -> None; rekurencyjnie czyści struktury."""
+ if isinstance(obj, float):
+ return None if (math.isnan(obj) or math.isinf(obj)) else obj
+ if isinstance(obj, (int, str)) or obj is None:
return obj
if isinstance(obj, dict):
return {k: _to_native(v) for k, v in obj.items()}
@@ -18,16 +46,17 @@ def _to_native(obj: Any):
def save_json(path: str, data: Any) -> None:
with open(path, "w", encoding="utf-8") as f:
- json.dump(_to_native(data), f, ensure_ascii=False, indent=2)
+ json.dump(_to_native(data), f, ensure_ascii=False, indent=2, allow_nan=False)
class Portfolio:
"""
- Prosta symulacja portfela:
- - cash
- - positions: {ticker: {"qty": float, "entry": float, "side": int}}
- - equity = cash + niezrealizowany PnL
- Logi transakcji z realized PnL (wartość i %), plus PnL skumulowany.
- Zapis tylko w JSON.
+ LONG open: cash -= qty*price ; close: cash += qty*price ; PnL = (px - entry)*qty
+ SHORT open (stock): cash += qty*price ; close: cash -= qty*price ; PnL = (entry - px)*qty
+ SHORT open (margin): cash bez zmian ; close: cash += PnL ; PnL = (entry - px)*qty
+
+ total_value:
+ - stock -> cash + positions_net (wartość likwidacyjna)
+ - margin -> start_capital + realized_pnl + unrealized_pnl
"""
def __init__(self, capital: float):
self.cash = float(capital)
@@ -35,103 +64,270 @@ class Portfolio:
self.positions: Dict[str, Dict] = {}
self.history: List[Dict] = []
self.trade_log: List[Dict] = []
- self.realized_pnl: float = 0.0 # skumulowany realized PnL
+ self.realized_pnl: float = 0.0
+ self.last_prices: Dict[str, float] = {} # ostatnie znane ceny
- def mark_to_market(self, prices: Dict[str, float]) -> float:
- unreal = 0.0
+ # ---------- helpers ----------
+ def _get_params(self, ticker: str):
+ o = SYMBOL_OVERRIDES.get(ticker, {})
+ sl = float(o.get("SL_PCT", SL_PCT))
+ tp = float(o.get("TP_PCT", TP_PCT))
+ tr = float(o.get("TRAIL_PCT", TRAIL_PCT))
+ return sl, tp, tr
+
+ def _init_risk(self, ticker: str, entry: float, side: int, atr: Optional[float] = None):
+ """Zwraca: stop, take, trail_best, trail_stop, one_r."""
+ sl_pct, tp_pct, trail_pct = self._get_params(ticker)
+ use_atr = USE_ATR and atr is not None and isinstance(atr, (int, float)) and atr > 0.0
+
+ if use_atr:
+ sl_dist = SL_ATR_MULT * float(atr)
+ tp_dist = TP_ATR_MULT * float(atr)
+ stop = entry - sl_dist if side == 1 else entry + sl_dist
+ take = entry + tp_dist if side == 1 else entry - tp_dist
+ one_r = sl_dist
+ else:
+ if side == 1:
+ stop = entry * (1.0 - sl_pct); take = entry * (1.0 + tp_pct)
+ else:
+ stop = entry * (1.0 + sl_pct); take = entry * (1.0 - tp_pct)
+ one_r = abs(entry - stop)
+
+ trail_best = entry
+ trail_stop = stop
+ return stop, take, trail_best, trail_stop, max(one_r, 1e-8)
+
+ def _update_trailing(self, ticker: str, pos: Dict, price: float):
+ """Breakeven po 1R + trailing procentowy."""
+ _, _, trail_pct = self._get_params(ticker)
+
+ # BE po 1R
+ if pos["side"] == 1 and not pos.get("be_done", False):
+ if price >= pos["entry"] + pos["one_r"]:
+ pos["stop"] = max(pos["stop"], pos["entry"])
+ pos["be_done"] = True
+ elif pos["side"] == -1 and not pos.get("be_done", False):
+ if price <= pos["entry"] - pos["one_r"]:
+ pos["stop"] = min(pos["stop"], pos["entry"])
+ pos["be_done"] = True
+
+ # trailing
+ if pos["side"] == 1:
+ if price > pos["trail_best"]:
+ pos["trail_best"] = price
+ pos["trail_stop"] = pos["trail_best"] * (1.0 - trail_pct)
+ else:
+ if price < pos["trail_best"]:
+ pos["trail_best"] = price
+ pos["trail_stop"] = pos["trail_best"] * (1.0 + trail_pct)
+
+ def _positions_values(self, prices: Optional[Dict[str, float]] = None) -> Dict[str, float]:
+ """Zwraca Σ|qty*px| oraz Σqty*px*side. Fallback ceny: prices -> self.last_prices -> entry."""
+ gross = 0.0
+ net = 0.0
for t, p in self.positions.items():
- price = prices.get(t)
- if price is not None and not math.isnan(price):
- unreal += (price - p["entry"]) * p["qty"] * p["side"]
- return self.cash + unreal
+ px = None
+ if prices is not None:
+ px = prices.get(t)
+ if px is None or (isinstance(px, float) and math.isnan(px)):
+ px = self.last_prices.get(t)
+ if px is None or (isinstance(px, float) and math.isnan(px)):
+ px = p["entry"]
+ gross += abs(p["qty"] * px)
+ net += p["qty"] * px * p["side"]
+ return {"positions_gross": gross, "positions_net": net}
+
+ def unrealized_pnl(self, prices: Dict[str, float]) -> float:
+ """Σ side * (px - entry) * qty."""
+ upnl = 0.0
+ for t, p in self.positions.items():
+ px = prices.get(t, self.last_prices.get(t, p["entry"]))
+ if px is None or (isinstance(px, float) and math.isnan(px)): px = p["entry"]
+ upnl += p["side"] * (px - p["entry"]) * p["qty"]
+ return upnl
+
+ def equity_from_start(self, prices: Dict[str, float]) -> float:
+ return self.start_capital + self.realized_pnl + self.unrealized_pnl(prices)
+
+ # ---------- zamknięcia / log ----------
+ def _close_position(self, ticker: str, price: float, reason: str):
+ pos = self.positions.get(ticker)
+ if not pos: return
+ qty, entry, side = pos["qty"], pos["entry"], pos["side"]
+
+ if side == 1:
+ self.cash += qty * price
+ pnl_abs = (price - entry) * qty
+ else:
+ pnl_abs = (entry - price) * qty
+ if ACCOUNTING_MODE == "stock":
+ self.cash -= qty * price
+ else:
+ self.cash += pnl_abs # margin: tylko PnL wpływa na cash
+
+ denom = max(qty * entry, 1e-12)
+ pnl_pct = pnl_abs / denom
+ self.realized_pnl += pnl_abs
+
+ self._log_trade("SELL", ticker, price, qty, side, pnl_abs, pnl_pct, reason=reason)
+ del self.positions[ticker]
def _log_trade(self, action: str, ticker: str, price: float, qty: float, side: int,
- pnl_abs: float = 0.0, pnl_pct: float = 0.0):
+ pnl_abs: float = 0.0, pnl_pct: float = 0.0, reason: Optional[str] = None):
ts = time.strftime("%Y-%m-%d %H:%M:%S")
- side_str = "LONG" if side == 1 else "SHORT"
if action == "BUY":
- print(f"[{ts}] BUY {ticker} @ {price:.6f}, qty={qty:.6f}, side={side_str}, cash={self.cash:.2f}")
- elif action == "SELL":
- print(f"[{ts}] SELL {ticker} @ {price:.6f}, qty={qty:.6f}, "
- f"PnL={pnl_abs:+.2f} ({pnl_pct:+.2%}), "
- f"cumPnL={self.realized_pnl:+.2f}, cash={self.cash:.2f}")
- # zapis do logu w pamięci
+ print(f"[{ts}] BUY {ticker} @ {price:.6f}, qty={qty:.6f}, side={'LONG' if side==1 else 'SHORT'}, cash={self.cash:.2f}")
+ else:
+ r = f", reason={reason}" if reason else ""
+ print(f"[{ts}] SELL {ticker} @ {price:.6f}, qty={qty:.6f}, PnL={pnl_abs:+.2f} ({pnl_pct:+.2%}), cash={self.cash:.2f}{r}")
self.trade_log.append({
- "time": ts,
- "action": action,
- "ticker": ticker,
- "price": float(price),
- "qty": float(qty),
- "side": int(side),
- "pnl_abs": float(pnl_abs),
- "pnl_pct": float(pnl_pct),
- "realized_pnl_cum": float(self.realized_pnl),
- "cash_after": float(self.cash)
+ "time": ts, "action": action, "ticker": ticker, "price": float(price),
+ "qty": float(qty), "side": int(side),
+ "pnl_abs": float(pnl_abs), "pnl_pct": float(pnl_pct),
+ "realized_pnl_cum": float(self.realized_pnl), "cash_after": float(self.cash),
+ "reason": reason or ""
})
+ # ---------- główna aktualizacja ----------
def on_signals(self, sigs: List[dict]):
- """
- sigs: lista dictów/obiektów z polami: ticker, price, signal, time
- BUY (1) / SELL (-1) / HOLD (0)
- """
- clean = []
+ # normalizacja
+ clean: List[Dict[str, Any]] = []
for s in sigs:
if isinstance(s, dict):
- if s.get("error") is None and s.get("price") is not None and not math.isnan(s.get("price", float("nan"))):
+ px = s.get("price")
+ if s.get("error") is None and px is not None and not math.isnan(px):
clean.append(s)
else:
if getattr(s, "error", None) is None and not math.isnan(getattr(s, "price", float("nan"))):
- clean.append({"ticker": s.ticker, "price": s.price, "signal": s.signal, "time": s.time})
+ clean.append({
+ "ticker": s.ticker, "price": s.price, "signal": s.signal, "time": s.time,
+ "atr": getattr(s, "atr", None)
+ })
+ # snapshot, gdy brak świeżych danych
if not clean:
+ vals = self._positions_values(None)
+ prices_map = {t: self.last_prices.get(t, p["entry"]) for t, p in self.positions.items()}
+ if ACCOUNTING_MODE == "stock":
+ account_value = self.cash + vals["positions_net"]
+ else:
+ account_value = self.equity_from_start(prices_map)
+ unreal = self.unrealized_pnl(prices_map)
self.history.append({
"time": time.strftime("%Y-%m-%d %H:%M:%S"),
- "equity": self.cash,
- "cash": self.cash,
- "open_positions": len(self.positions)
+ "cash": float(self.cash),
+ "positions_value": float(vals["positions_gross"]),
+ "positions_net": float(vals["positions_net"]),
+ "total_value": float(account_value),
+ "equity_from_start": float(self.start_capital + self.realized_pnl + unreal),
+ "unrealized_pnl": float(unreal),
+ "realized_pnl_cum": float(self.realized_pnl),
+ "open_positions": int(len(self.positions))
})
save_json("portfolio_history.json", self.history)
return
- prices = {s["ticker"]: s["price"] for s in clean}
- n = len(clean)
- per_trade_cash = max(self.cash / (n * 2), 0.0)
+ # mapy cen
+ prices = {s["ticker"]: float(s["price"]) for s in clean}
+ self.last_prices.update(prices)
+ # 1) risk exits (SL/TP/TRAIL + BE)
+ to_close = []
+ for t, pos in list(self.positions.items()):
+ price = prices.get(t)
+ if price is None or math.isnan(price): continue
+ self._update_trailing(t, pos, price)
+ if pos["side"] == 1:
+ if price <= pos["stop"]: to_close.append((t, "SL"))
+ elif price >= pos["take"]: to_close.append((t, "TP"))
+ elif price <= pos.get("trail_stop", -float("inf")): to_close.append((t, "TRAIL"))
+ else:
+ if price >= pos["stop"]: to_close.append((t, "SL"))
+ elif price <= pos["take"]: to_close.append((t, "TP"))
+ elif price >= pos.get("trail_stop", float("inf")): to_close.append((t, "TRAIL"))
+ for t, reason in to_close:
+ price = prices.get(t)
+ if price is not None and not math.isnan(price) and t in self.positions:
+ self._close_position(t, price, reason=reason)
+
+ # 2) zamknięcie na przeciwny/HOLD
for s in clean:
- t, sig, price = s["ticker"], int(s["signal"]), float(s["price"])
-
- # zamknięcie lub odwrócenie
+ t, sig = s["ticker"], int(s["signal"])
+ price = float(s["price"])
if t in self.positions:
pos = self.positions[t]
if sig == 0 or sig != pos["side"]:
- qty = pos["qty"]
- entry = pos["entry"]
- side = pos["side"]
+ self._close_position(t, price, reason="SIGNAL")
+
+ # 3) otwarcia – ATR sizing + limity
+ candidates = [s for s in clean if s["ticker"] not in self.positions and int(s["signal"]) != 0]
+ if MAX_NEW_POSITIONS_PER_CYCLE and MAX_NEW_POSITIONS_PER_CYCLE > 0:
+ candidates = candidates[:MAX_NEW_POSITIONS_PER_CYCLE]
+
+ for s in candidates:
+ t, sig, price = s["ticker"], int(s["signal"]), float(s["price"])
+ atr = s.get("atr", None)
+ if price <= 0: continue
+
+ stop, take, trail_best, trail_stop, one_r = self._init_risk(t, price, sig, atr)
+
+ # equity teraz (dla sizingu ryzyka)
+ if ACCOUNTING_MODE == "margin":
+ equity_now = self.start_capital + self.realized_pnl + self.unrealized_pnl(self.last_prices)
+ else:
+ vals_tmp = self._positions_values(self.last_prices)
+ equity_now = self.cash + vals_tmp["positions_net"]
+
+ risk_amount = max(1e-6, equity_now * RISK_FRACTION)
+ qty_risk = risk_amount / max(one_r, 1e-8)
+
+ # limit gotówki dla LONG
+ per_trade_cash = max(MIN_TRADE_CASH, self.cash * ALLOC_FRACTION)
+ qty_cash = per_trade_cash / max(price, 1e-12) if sig == 1 else float("inf")
+
+ qty = max(0.0, min(qty_risk, qty_cash))
+ if qty <= 0: continue
+
+ # księgowanie gotówki przy otwarciu
+ if sig == 1:
+ cost = qty * price
+ if self.cash < cost: continue
+ self.cash -= cost
+ else:
+ if ACCOUNTING_MODE == "stock":
self.cash += qty * price
- pnl_abs = (price - entry) * qty * side
- denom = max(qty * entry, 1e-12)
- pnl_pct = pnl_abs / denom
- self.realized_pnl += pnl_abs
- self._log_trade("SELL", t, price, qty, side, pnl_abs, pnl_pct)
- del self.positions[t]
+ # margin: brak zmiany cash przy short open
- # otwarcie
- if t not in self.positions and sig != 0 and per_trade_cash > 0:
- qty = per_trade_cash / price
- self.cash -= qty * price
- self.positions[t] = {"qty": qty, "entry": price, "side": sig}
- self._log_trade("BUY", t, price, qty, sig)
+ self.positions[t] = {
+ "qty": qty, "entry": price, "side": sig,
+ "stop": stop, "take": take,
+ "trail_best": trail_best, "trail_stop": trail_stop,
+ "one_r": one_r, "be_done": False
+ }
+ self._log_trade("BUY" if sig == 1 else "SELL", t, price, qty, sig)
+
+ # 4) snapshot na bieżących/ostatnich cenach
+ vals = self._positions_values(self.last_prices)
+ prices_map = {t: self.last_prices.get(t, p["entry"]) for t, p in self.positions.items()}
+ if ACCOUNTING_MODE == "stock":
+ account_value = self.cash + vals["positions_net"]
+ else:
+ account_value = self.equity_from_start(prices_map)
+ unreal = self.unrealized_pnl(prices_map)
- equity = self.mark_to_market(prices)
self.history.append({
"time": clean[0]["time"],
- "equity": float(equity),
"cash": float(self.cash),
- "open_positions": int(len(self.positions)),
- "realized_pnl_cum": float(self.realized_pnl)
+ "positions_value": float(vals["positions_gross"]),
+ "positions_net": float(vals["positions_net"]),
+ "total_value": float(account_value),
+ "equity_from_start": float(self.start_capital + self.realized_pnl + unreal),
+ "unrealized_pnl": float(unreal),
+ "realized_pnl_cum": float(self.realized_pnl),
+ "open_positions": int(len(self.positions))
})
- # zapis plików JSON
+ # 5) zapisy
save_json("trade_log.json", self.trade_log)
save_json("portfolio_history.json", self.history)
save_json("positions.json", [{"ticker": t, **p} for t, p in self.positions.items()])
diff --git a/portfolio_history.json b/portfolio_history.json
new file mode 100644
index 0000000..f552e93
--- /dev/null
+++ b/portfolio_history.json
@@ -0,0 +1,24 @@
+[
+ {
+ "time": "2025-08-15 10:29:00+00:00",
+ "cash": 10000.0,
+ "positions_value": 2999.9999999999804,
+ "positions_net": -2999.9999999999804,
+ "total_value": 10000.0,
+ "equity_from_start": 10000.0,
+ "unrealized_pnl": 0.0,
+ "realized_pnl_cum": 0.0,
+ "open_positions": 1
+ },
+ {
+ "time": "2025-08-15 10:30:00+00:00",
+ "cash": 9996.027808030158,
+ "positions_value": 0.0,
+ "positions_net": 0.0,
+ "total_value": 9996.027808030158,
+ "equity_from_start": 9996.027808030158,
+ "unrealized_pnl": 0.0,
+ "realized_pnl_cum": -3.972191969841845,
+ "open_positions": 0
+ }
+]
\ No newline at end of file
diff --git a/positions.json b/positions.json
new file mode 100644
index 0000000..0637a08
--- /dev/null
+++ b/positions.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/pythonProject.zip b/pythonProject.zip
deleted file mode 100644
index 9bf7ff511691759240e92bedb633719bfb970875..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 61134
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 (x == null || isNaN(x) ? "—" : Number(x).toFixed(2));
+const fmt6 = (x) => (x == null || isNaN(x) ? "—" : Number(x).toFixed(6));
+const fmtPct= (x) => (x == null || isNaN(x) ? "—" : (Number(x) * 100).toFixed(2) + "%");
-// ── utils ──────────────────────────────────────────────────────────────────────
-const fmtNum = (x, d = 2) =>
- x === null || x === undefined || Number.isNaN(x) ? "–" : Number(x).toFixed(d);
-
-// Potencjalne adresy backendu (API). Pierwszy to aktualny origin UI.
-const apiCandidates = [
- window.location.origin,
- "http://127.0.0.1:8000",
- "http://localhost:8000",
- "http://172.27.20.120:8000", // z logów serwera
-];
-
-let API_BASE = null;
-let warnedMixed = false;
-
-function withTimeout(ms = 6000) {
- const ctrl = new AbortController();
- const id = setTimeout(() => ctrl.abort("timeout"), ms);
- return { signal: ctrl.signal, done: () => clearTimeout(id) };
+async function getJSON(url) {
+ const r = await fetch(url, { cache: "no-store" });
+ if (!r.ok) throw new Error(`${url}: ${r.status}`);
+ return r.json();
}
-function makeUrl(path) {
- return `${API_BASE}${path}${path.includes("?") ? "&" : "?"}_ts=${Date.now()}`;
-}
+// prosty wykres linii na canvas (bez bibliotek)
+function drawLineChart(canvas, points) {
+ const ctx = canvas.getContext("2d");
+ const pad = 32;
+ const w = canvas.width, h = canvas.height;
+ ctx.clearRect(0, 0, w, h);
-function setBadge(id, text, ok = true) {
- const el = document.getElementById(id);
- if (!el) return;
- el.textContent = text;
- el.className = ok ? "badge ok" : "badge err";
-}
-
-function setBadgeTitle(id, title) {
- const el = document.getElementById(id);
- if (el) el.title = title || "";
-}
-
-function warnMixedContent(base) {
- if (!warnedMixed && location.protocol === "https:" && base?.startsWith("http://")) {
- warnedMixed = true;
- console.warn(
- "[api] UI działa przez HTTPS, a API przez HTTP — przeglądarka może blokować żądania (mixed content)."
- );
- alert(
- "UI działa przez HTTPS, a API przez HTTP. Uruchom UI przez HTTP albo włącz HTTPS dla API — inaczej przeglądarka zablokuje żądania."
- );
+ if (!points || points.length === 0) {
+ ctx.fillStyle = "#9aa3b2";
+ ctx.fillText("Brak danych", 10, 20);
+ return;
}
+
+ const n = points.length;
+ const ys = points.map(p => p.y);
+ const minY = Math.min(...ys), maxY = Math.max(...ys);
+ const yLo = minY === maxY ? minY - 1 : minY;
+ const yHi = minY === maxY ? maxY + 1 : maxY;
+
+ const x0 = pad, y0 = h - pad, x1 = w - pad, y1 = pad;
+ const xScale = (i) => x0 + (i / (n - 1)) * (x1 - x0);
+ const yScale = (y) => y0 - ((y - yLo) / (yHi - yLo)) * (y0 - y1);
+
+ // osie
+ ctx.strokeStyle = "#242a36";
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ ctx.moveTo(x0, y0); ctx.lineTo(x1, y0);
+ ctx.moveTo(x0, y0); ctx.lineTo(x0, y1);
+ ctx.stroke();
+
+ // siatka
+ ctx.strokeStyle = "#1b2130";
+ [0.25, 0.5, 0.75].forEach(f => {
+ const yy = y0 - (y0 - y1) * f;
+ ctx.beginPath();
+ ctx.moveTo(x0, yy); ctx.lineTo(x1, yy);
+ ctx.stroke();
+ });
+
+ // podpisy min/max
+ ctx.fillStyle = "#9aa3b2";
+ ctx.font = "12px system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial";
+ ctx.fillText(fmt2(yHi), 6, y1 + 10);
+ ctx.fillText(fmt2(yLo), 6, y0 - 2);
+
+ // linia
+ ctx.strokeStyle = "#4da3ff";
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.moveTo(xScale(0), yScale(ys[0]));
+ for (let i = 1; i < n; i++) ctx.lineTo(xScale(i), yScale(ys[i]));
+ ctx.stroke();
+
+ // kropka na końcu
+ const lastX = xScale(n - 1), lastY = yScale(ys[n - 1]);
+ ctx.fillStyle = "#e7e9ee";
+ ctx.beginPath(); ctx.arc(lastX, lastY, 3, 0, Math.PI * 2); ctx.fill();
+ ctx.fillText(fmt2(ys[n - 1]), lastX + 6, lastY - 6);
}
-// ── autodetekcja backendu ─────────────────────────────────────────────────────
-async function pickBackend() {
- for (const base of apiCandidates) {
- try {
- const t = withTimeout(2500);
- const r = await fetch(`${base}/api/status?_ts=${Date.now()}`, {
- cache: "no-store",
- signal: t.signal,
- });
- t.done();
- if (r.ok) {
- API_BASE = base;
- console.debug("[api] using", API_BASE);
- warnMixedContent(API_BASE);
- setBadgeTitle("loopState", `API: ${API_BASE}`);
- return;
- }
- console.debug("[api] probe", base, "->", r.status);
- } catch (e) {
- // ignorujemy i próbujemy kolejny kandydat
- console.debug("[api] probe fail", base, e?.message || e);
- }
- }
- throw new Error("Nie znaleziono działającego backendu (API_BASE). Upewnij się, że server.py działa na porcie 8000.");
-}
-
-// ── API helpers ───────────────────────────────────────────────────────────────
-async function apiGet(path) {
- const url = makeUrl(path);
- const t0 = performance.now();
- const t = withTimeout(6000);
+async function loadAll() {
try {
- const r = await fetch(url, { cache: "no-store", signal: t.signal });
- const t1 = performance.now();
- console.debug("[api] GET", url, r.status, (t1 - t0).toFixed(1) + "ms");
- if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
- return r.json();
- } finally {
- t.done();
- }
-}
+ const [snap, hist, pos, trades] = await Promise.all([
+ getJSON("/api/snapshot"),
+ getJSON("/api/history"),
+ getJSON("/api/positions"),
+ getJSON("/api/trades"),
+ ]);
-async function apiPost(path, body) {
- const url = makeUrl(path);
- const t0 = performance.now();
- const t = withTimeout(6000);
- try {
- const r = await fetch(url, {
- method: "POST",
- headers: body ? { "Content-Type": "application/json" } : undefined,
- body: body ? JSON.stringify(body) : undefined,
- signal: t.signal,
+ // czas
+ document.getElementById("last-update").textContent =
+ "Ostatnia aktualizacja: " + (snap?.last_history?.time || "—");
+
+ // ===== ostatni wiersz historii =====
+ const last = Array.isArray(hist) && hist.length ? hist[hist.length - 1] : null;
+
+ const cashVal = Number(last?.cash ?? 0);
+ const totalVal = Number(
+ last?.total_value ??
+ (Number(last?.cash ?? 0) + Number(last?.positions_net ?? 0)) // fallback dla starszych logów
+ );
+
+ // KARTY
+ document.getElementById("cash").textContent = fmt2(cashVal);
+ document.getElementById("total-value").textContent = fmt2(totalVal);
+
+ // mapa ostatnich cen do liczenia zysku (unrealized)
+ const lastPrice = new Map();
+ (snap?.signals || []).forEach(s => {
+ const px = Number(s.price);
+ if (!isNaN(px)) lastPrice.set(s.ticker, px);
});
- const t1 = performance.now();
- console.debug("[api] POST", url, r.status, (t1 - t0).toFixed(1) + "ms");
- if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
- return r.json();
- } finally {
- t.done();
- }
-}
-// ── refreshers ────────────────────────────────────────────────────────────────
-async function refreshStatus() {
- try {
- const s = await apiGet("/api/status");
- setBadge("loopState", s.running ? "RUNNING" : "STOPPED", s.running);
- setBadgeTitle("loopState", `API: ${API_BASE} | last_action=${s.last_action || "–"}`);
+ // Zysk = suma niezrealizowanych PnL na otwartych pozycjach
+ let unrealPnL = 0;
+ (pos || []).forEach(p => {
+ const price = Number(lastPrice.get(p.ticker));
+ const entry = Number(p.entry);
+ const qty = Number(p.qty);
+ const side = Number(p.side);
+ if (isNaN(price) || isNaN(entry) || isNaN(qty) || isNaN(side)) return;
+ unrealPnL += side === 1 ? (price - entry) * qty : (entry - price) * qty;
+ });
+ const unrlEl = document.getElementById("unrealized");
+ unrlEl.textContent = fmt2(unrealPnL);
+ unrlEl.classList.remove("pnl-positive", "pnl-negative");
+ if (unrealPnL > 0) unrlEl.classList.add("pnl-positive");
+ else if (unrealPnL < 0) unrlEl.classList.add("pnl-negative");
- const roundEl = document.getElementById("roundNo");
- if (roundEl) roundEl.textContent = s.round ?? "–";
+ // liczba otwartych pozycji
+ document.getElementById("open-pos").textContent =
+ Number(last?.open_positions ?? (pos?.length ?? 0));
- const cashEl = document.getElementById("cash");
- if (cashEl) cashEl.textContent = fmtNum(s.cash, 2);
+ // ===== WYKRES WARTOŚCI KONTA (TOTAL) =====
+ const totalCanvas = document.getElementById("totalChart");
+ const totalPoints = (hist || [])
+ .map((row, i) => {
+ const v = (row.total_value != null)
+ ? Number(row.total_value)
+ : Number(row.cash ?? 0) + Number(row.positions_net ?? 0);
+ return { x: i, y: v };
+ })
+ .filter(p => !isNaN(p.y))
+ .slice(-500);
+ drawLineChart(totalCanvas, totalPoints);
- // now-playing
- const stageEl = document.getElementById("stage");
- if (stageEl) stageEl.textContent = (s.stage || "idle").toUpperCase();
+ // ===== WYKRES GOTÓWKI (CASH) =====
+ const cashCanvas = document.getElementById("cashChart");
+ const cashPoints = (hist || [])
+ .map((row, i) => ({ x: i, y: Number(row.cash ?? NaN) }))
+ .filter(p => !isNaN(p.y))
+ .slice(-500);
+ drawLineChart(cashCanvas, cashPoints);
- const tickerEl = document.getElementById("ticker");
- if (tickerEl) tickerEl.textContent = s.current_ticker || "–";
+ // ===== POZYCJE =====
+ const posBody = document.querySelector("#positions-table tbody");
+ posBody.innerHTML = "";
+ (pos || []).forEach((p) => {
+ const price = Number(lastPrice.get(p.ticker));
+ const entry = Number(p.entry);
+ const qty = Number(p.qty);
+ const side = Number(p.side);
- const idx = s.current_index ?? 0;
- const total = s.tickers_total ?? 0;
+ let upnl = NaN, upct = NaN;
+ if (!isNaN(price) && !isNaN(entry) && !isNaN(qty) && !isNaN(side)) {
+ upnl = side === 1 ? (price - entry) * qty : (entry - price) * qty;
+ const denom = Math.max(qty * entry, 1e-12);
+ upct = upnl / denom;
+ }
+ const pnlClass = upnl > 0 ? "pnl-positive" : (upnl < 0 ? "pnl-negative" : "");
- const progressTextEl = document.getElementById("progressText");
- if (progressTextEl) progressTextEl.textContent = `${Math.min(idx + 1, total)} / ${total}`;
-
- const prog = document.getElementById("progress");
- if (prog) {
- prog.max = total || 1;
- prog.value = Math.min(idx + 1, total) || 0;
- }
-
- const lastActionEl = document.getElementById("lastAction");
- if (lastActionEl) lastActionEl.textContent = s.last_action || "–";
- } catch (e) {
- console.error("status error:", e);
- setBadge("loopState", "ERR", false);
- setBadgeTitle("loopState", `API: ${API_BASE || "—"} | ${e?.message || e}`);
- }
-}
-
-async function refreshPositions() {
- try {
- const { positions } = await apiGet("/api/positions");
- const tbody = document.querySelector("#positions tbody");
- if (!tbody) return;
- tbody.innerHTML = "";
- for (const p of positions) {
const tr = document.createElement("tr");
tr.innerHTML = `
| ${p.ticker} |
- ${p.side} |
- ${fmtNum(p.size, 0)} |
- ${fmtNum(p.entry_price)} |
- ${fmtNum(p.last_price)} |
- ${fmtNum(p.pnl)} |
+ ${fmt6(qty)} |
+ ${fmt6(entry)} |
+ ${side === 1 ? "LONG" : "SHORT"} |
+ ${fmt6(price)} |
+ ${fmt2(upnl)} |
+ ${fmtPct(upct)} |
`;
- tbody.appendChild(tr);
- }
- } catch (e) {
- console.error("positions error:", e);
- }
-}
+ posBody.appendChild(tr);
+ });
-async function refreshTrades() {
- try {
- const { trades } = await apiGet("/api/trades");
- const tbody = document.querySelector("#trades tbody");
- if (!tbody) return;
- tbody.innerHTML = "";
- trades.slice(-50).reverse().forEach((t) => {
+ // ===== SYGNAŁY =====
+ const sigBody = document.querySelector("#signals-table tbody");
+ sigBody.innerHTML = "";
+ const signals = (snap?.signals || []).slice().sort((a,b)=>a.ticker.localeCompare(b.ticker));
+ signals.forEach((s) => {
+ const sigTxt = s.signal === 1 ? "BUY" : (s.signal === -1 ? "SELL" : "HOLD");
const tr = document.createElement("tr");
tr.innerHTML = `
- ${t.time ?? ""} |
- ${t.ticker ?? ""} |
- ${t.action ?? ""} |
- ${fmtNum(t.price)} |
- ${fmtNum(t.size, 0)} |
+ ${s.ticker} |
+ ${s.time} |
+ ${fmt6(s.price)} |
+ ${sigTxt} |
+ ${s.interval} |
`;
- tbody.appendChild(tr);
+ sigBody.appendChild(tr);
});
+
+ // ===== TRANSAKCJE =====
+ const tradesBody = document.querySelector("#trades-table tbody");
+ tradesBody.innerHTML = "";
+ (trades || []).slice(-50).reverse().forEach((t) => {
+ const pnlClass = t.pnl_abs > 0 ? "pnl-positive" : (t.pnl_abs < 0 ? "pnl-negative" : "");
+ const tr = document.createElement("tr");
+ tr.innerHTML = `
+ ${t.time} |
+ ${t.action} |
+ ${t.ticker} |
+ ${fmt6(t.price)} |
+ ${fmt6(t.qty)} |
+ ${fmt2(t.pnl_abs)} |
+ ${fmtPct(t.pnl_pct)} |
+ ${fmt2(t.cash_after)} |
+ `;
+ tradesBody.appendChild(tr);
+ });
+
} catch (e) {
- console.error("trades error:", e);
+ console.error("loadAll error:", e);
+ document.getElementById("last-update").textContent = "Błąd ładowania danych";
}
}
-async function refreshAll() {
- await Promise.all([refreshStatus(), refreshPositions(), refreshTrades()]);
-}
-
-// ── auto refresh ──────────────────────────────────────────────────────────────
-let timer = null;
-let currentInterval = 2000; // domyślnie 2s (zgodne z Twoim selectem)
-
-function startAutoRefresh() {
- if (timer) return;
- timer = setInterval(refreshAll, currentInterval);
- console.debug("[ui] auto refresh started", currentInterval, "ms");
-}
-function stopAutoRefresh() {
- if (!timer) return;
- clearInterval(timer);
- timer = null;
- console.debug("[ui] auto refresh stopped");
-}
-
-// ── bootstrap ────────────────────────────────────────────────────────────────
-document.addEventListener("DOMContentLoaded", async () => {
- // Przyciski
- document.getElementById("btnStart")?.addEventListener("click", async () => {
- try { await apiPost("/api/start"); await refreshStatus(); } catch (e) { console.error(e); }
- });
- document.getElementById("btnStop")?.addEventListener("click", async () => {
- try { await apiPost("/api/stop"); await refreshStatus(); } catch (e) { console.error(e); }
- });
- document.getElementById("btnTick")?.addEventListener("click", async () => {
- try { await apiPost("/api/run-once"); await refreshAll(); } catch (e) { console.error(e); }
- });
-
- const sel = document.getElementById("refreshMs");
- sel?.addEventListener("change", () => {
- currentInterval = parseInt(sel.value, 10);
- if (timer) { stopAutoRefresh(); startAutoRefresh(); }
- });
-
- document.getElementById("autoOn")?.addEventListener("click", startAutoRefresh);
- document.getElementById("autoOff")?.addEventListener("click", stopAutoRefresh);
-
- // Autodetekcja backendu przed pierwszym odświeżeniem
- try {
- await pickBackend();
- await refreshAll();
- startAutoRefresh();
- } catch (e) {
- console.error(e);
- setBadge("loopState", "NO API", false);
- setBadgeTitle("loopState", e?.message || String(e));
- alert("UI nie może połączyć się z backendem (port 8000). Uruchom server.py lub zaktualizuj API_BASE w app.js.");
- }
-});
+document.getElementById("refresh-btn").addEventListener("click", loadAll);
+loadAll();
+setInterval(loadAll, 1000);
diff --git a/static/style.css b/static/style.css
deleted file mode 100644
index 17f4d82..0000000
--- a/static/style.css
+++ /dev/null
@@ -1,41 +0,0 @@
-:root{
- --border:#e5e7eb;
- --muted:#64748b;
-}
-*{ box-sizing:border-box; }
-body{ font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; margin:0; color:#0f172a; background:#fff; }
-.container{ max-width: 1100px; margin: 0 auto; padding: 16px; }
-header{ display:flex; align-items:center; justify-content:space-between; margin-bottom: 16px; gap: 12px; }
-h1{ margin:0; font-size: 22px; }
-h2{ font-size:18px; margin: 18px 0 10px; }
-h3{ font-size:16px; margin:0; }
-
-#statusBar{ display:flex; align-items:center; gap: 12px; flex-wrap: wrap; }
-#statusBar span{ font-size:14px; color:#0f172a; }
-.btn{
- appearance:none; border:1px solid var(--border); background:#0f172a; color:#fff;
- padding:8px 12px; border-radius:10px; cursor:pointer; font-weight:600;
-}
-.btn:hover{ opacity:.9; }
-.btn-secondary{ background:#475569; }
-
-.grid{ display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin: 12px 0 24px; }
-.card{ background:#0f172a0d; border:1px solid var(--border); border-radius:12px; padding:12px; }
-.kpi{ font-size: 24px; font-weight: 700; margin-top: 8px; }
-.table-wrap{ overflow:auto; border:1px solid var(--border); border-radius:12px; }
-table{ width:100%; border-collapse: collapse; font-size: 14px; }
-th, td{ padding: 8px 10px; border-bottom: 1px solid #f1f5f9; white-space: nowrap; }
-thead th{ position: sticky; top: 0; background: #fff; z-index: 1; }
-tbody tr:hover{ background:#f8fafc; }
-
-.pnl-pos{ color: #166534; font-weight:600; }
-.pnl-neg{ color: #991b1b; font-weight:600; }
-
-.badge{
- padding:3px 8px; border-radius:999px; font-size:12px; border:1px solid var(--border);
- display:inline-block;
-}
-.badge.long{ background:#ecfdf5; border-color:#bbf7d0; }
-.badge.short{ background:#fef2f2; border-color:#fecaca; }
-
-.empty{ text-align:center; color: var(--muted); padding: 14px; }
diff --git a/new/static/styles.css b/static/styles.css
similarity index 100%
rename from new/static/styles.css
rename to static/styles.css
diff --git a/new/strategies.py b/strategies.py
similarity index 100%
rename from new/strategies.py
rename to strategies.py
diff --git a/strategy.py b/strategy.py
deleted file mode 100644
index bbb3c2b..0000000
--- a/strategy.py
+++ /dev/null
@@ -1,93 +0,0 @@
-from __future__ import annotations
-from dataclasses import dataclass
-import math
-import numpy as np
-import pandas as pd
-from config import CFG
-# zakładam, że masz te funkcje gdzieś u siebie:
-# from indicators import ema, rsi, atr, macd, adx_val
-from indicators import ema, rsi, atr, macd, adx_val
-
-Signal = str # "BUY" | "SELL" | "NONE"
-
-@dataclass
-class Decision:
- signal: Signal
- sl: float | None
- tp: float | None
- rpu: float # risk per unit (odległość od SL)
-
-def evaluate_signal(df: pd.DataFrame) -> Decision:
- """
- Warunki (poluzowane / opcjonalne):
- - (opcjonalnie) kierunek EMA200 (trend filter),
- - ADX >= CFG.adx_min,
- - ATR >= CFG.atr_min_frac_price * price,
- - RSI nieprzeciążone (BUY < rsi_buy_max, SELL > rsi_sell_min),
- - (opcjonalnie) MACD zgodny z kierunkiem.
- Wyjście: SL = k_ATR, TP = RR * R (R = dystans do SL).
- """
- if df is None or len(df) < CFG.history_min_bars:
- return Decision("NONE", None, None, 0.0)
-
- # Bezpieczne pobranie kolumn
- cols = {c.lower(): c for c in df.columns}
- C = pd.to_numeric(df[cols.get("close","Close")], errors="coerce").to_numpy(float)
- H = pd.to_numeric(df[cols.get("high","High")], errors="coerce").to_numpy(float)
- L = pd.to_numeric(df[cols.get("low","Low")], errors="coerce").to_numpy(float)
-
- if len(C) == 0 or np.isnan(C[-1]):
- return Decision("NONE", None, None, 0.0)
-
- # wskaźniki
- ema200 = ema(C, 200)
- rsi14 = rsi(C, CFG.rsi_len)
- atr14 = atr(H, L, C, 14)
- macd_line, macd_sig, macd_hist = macd(C, 12, 26, 9)
- adx14, pdi, mdi = adx_val(H, L, C, 14)
-
- c = float(C[-1])
- a = float(atr14[-1]) if math.isfinite(atr14[-1]) else 0.0
-
- if not (math.isfinite(c) and math.isfinite(a)) or a <= 0:
- return Decision("NONE", None, None, 0.0)
-
- # filtry bazowe
- adx_ok = float(adx14[-1]) >= CFG.adx_min
- atr_ok = (a / max(1e-9, c)) >= CFG.atr_min_frac_price
-
- # trend (opcjonalny)
- trend_ok_buy = True if not CFG.require_trend else (c > float(ema200[-1]))
- trend_ok_sell = True if not CFG.require_trend else (c < float(ema200[-1]))
-
- # RSI „nieprzeciążone”
- rsi_val = float(rsi14[-1])
- rsi_ok_buy = rsi_val <= CFG.rsi_buy_max
- rsi_ok_sell = rsi_val >= CFG.rsi_sell_min
-
- # MACD (opcjonalny)
- if CFG.use_macd_filter:
- macd_buy = (macd_line[-1] > macd_sig[-1] and macd_hist[-1] > 0)
- macd_sell = (macd_line[-1] < macd_sig[-1] and macd_hist[-1] < 0)
- else:
- macd_buy = macd_sell = True
-
- signal: Signal = "NONE"
- sl = tp = None
- rpu = 0.0
-
- if adx_ok and atr_ok:
- if trend_ok_buy and rsi_ok_buy and macd_buy:
- signal = "BUY"
- sl = c - CFG.sl_atr_mult * a
- risk = c - sl
- tp = c + CFG.tp_rr * risk
- rpu = max(1e-9, risk)
- elif CFG.allow_short and trend_ok_sell and rsi_ok_sell and macd_sell:
- signal = "SELL"
- sl = c + CFG.sl_atr_mult * a
- risk = sl - c
- tp = c - CFG.tp_rr * risk
- rpu = max(1e-9, risk)
-
- return Decision(signal, sl, tp, rpu)
diff --git a/templates/index.html b/templates/index.html
index c55391a..da2a5a9 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -1,81 +1,114 @@
-
-
- Trader – Panel
-
-
+
+
+ Trading Dashboard
+
-
+
+
+ Wartość konta (total) — wykres
+
+
+
+
-
-
Teraz:
-
–
-
–
-
0 / 0
-
-
Ostatnia akcja:
-
–
-
+
+
+ Gotówka — wykres
+
+
+
+
-
-
-
Pozycje
-
+
+
+ Otwarte pozycje
+
- | Ticker | Strona | Ilość | Wejście | Ostatnia | PnL |
+
+ | Ticker |
+ Qty |
+ Entry |
+ Side |
+ Last price |
+ Unreal. PnL |
+ Unreal. PnL % |
+
-
-
-
Transakcje (ostatnie 50)
-
+
+
+
+
+ Ostatnie sygnały
+
- | Czas | Ticker | Akcja | Cena | Ilość |
+
+ | Ticker |
+ Time |
+ Price |
+ Signal |
+ Interval |
+
-
-
+
-
+
+
+ Transakcje (ostatnie 50)
+
+
+
+ | Time |
+ Action |
+ Ticker |
+ Price |
+ Qty |
+ PnL |
+ PnL % |
+ Cash po |
+
+
+
+
+
+
+
+
diff --git a/trade_log.json b/trade_log.json
new file mode 100644
index 0000000..77badb8
--- /dev/null
+++ b/trade_log.json
@@ -0,0 +1,28 @@
+[
+ {
+ "time": "2025-08-15 12:31:18",
+ "action": "SELL",
+ "ticker": "OP-USD",
+ "price": 0.7543854713439941,
+ "qty": 3976.7467878924763,
+ "side": -1,
+ "pnl_abs": 0.0,
+ "pnl_pct": 0.0,
+ "realized_pnl_cum": 0.0,
+ "cash_after": 10000.0,
+ "reason": ""
+ },
+ {
+ "time": "2025-08-15 12:32:18",
+ "action": "SELL",
+ "ticker": "OP-USD",
+ "price": 0.7553843259811401,
+ "qty": 3976.7467878924763,
+ "side": -1,
+ "pnl_abs": -3.972191969841845,
+ "pnl_pct": -0.0013240639899472903,
+ "realized_pnl_cum": -3.972191969841845,
+ "cash_after": 9996.027808030158,
+ "reason": "SIGNAL"
+ }
+]
\ No newline at end of file
diff --git a/trader.py b/trader.py
deleted file mode 100644
index 8c31d16..0000000
--- a/trader.py
+++ /dev/null
@@ -1,264 +0,0 @@
-# trader.py
-from __future__ import annotations
-
-import threading
-import time
-import logging
-from typing import Dict, List, Any, Optional
-
-import pandas as pd
-
-from config import CFG
-from strategy import evaluate_signal, Decision
-from portfolio import Portfolio
-
-# ————————————————————————————————————————————————————————————————
-# save_outputs – opcjonalny helper; jeżeli masz własny w util.py, użyje jego
-try:
- from util import save_outputs # def save_outputs(root, symbols, trades_df, equity_df, cash): ...
-except Exception:
- def save_outputs(root_dir: str, symbols: List[str], trades_df: pd.DataFrame,
- equity_df: pd.DataFrame, cash: float):
- # fallback no-op, żeby nie wysypywać serwera
- pass
-# ————————————————————————————————————————————————————————————————
-# fetch_batch – Twój moduł pobierania danych (w logach: "data | Yahoo: ...")
-from data import fetch_batch # def fetch_batch(tickers: List[str], period: str, interval: str) -> Dict[str, pd.DataFrame]
-
-log = logging.getLogger("trader")
-
-
-class TraderWorker:
- def __init__(self):
- self.portfolio = Portfolio(
- starting_cash=CFG.starting_cash,
- commission_per_trade=CFG.commission_per_trade,
- slippage_bp=CFG.slippage_bp,
- )
-
- self.syms: List[str] = CFG.tickers[:] # 40 szt.
- self.hist: Dict[str, pd.DataFrame] = {}
- self.last_ts: Dict[str, pd.Timestamp] = {}
-
- # stan pętli
- self._thread: Optional[threading.Thread] = None
- self._stop_evt = threading.Event()
- self._running = False
-
- # telemetry
- self._idx = 0
- self._round = 0
- self._stage = "idle"
- self._current_ticker = None
- self._last_action = None
- self._last_heartbeat = time.time()
-
- log.info("INIT: %d symbols | period=%s interval=%s cash=%.2f",
- len(self.syms), CFG.yf_period, CFG.interval, self.portfolio.cash)
-
- # ———————————————————— API do serwera ————————————————————
-
- def is_running(self) -> bool:
- return self._running
-
- def start(self) -> bool:
- if self._running:
- return False
- self._stop_evt.clear()
- self._thread = threading.Thread(target=self._loop, name="TraderLoop", daemon=True)
- self._thread.start()
- self._running = True
- log.info("LOOP: sequential 1-ticker | sleep=%ss", CFG.loop_sleep_s)
- log.info("LOOP: started")
- return True
-
- def stop(self) -> bool:
- if not self._running:
- return False
- self._stop_evt.set()
- if self._thread and self._thread.is_alive():
- self._thread.join(timeout=5)
- self._running = False
- log.info("LOOP: stopped")
- return True
-
- def _loop(self):
- while not self._stop_evt.is_set():
- self.tick_once()
- time.sleep(max(0, CFG.loop_sleep_s))
-
- def status(self) -> Dict[str, Any]:
- return {
- "running": self._running,
- "round": self._round,
- "stage": self._stage,
- "current_ticker": self._current_ticker,
- "current_index": self._idx % len(self.syms) if self.syms else 0,
- "tickers_total": len(self.syms),
- "last_action": self._last_action,
- "cash": float(self.portfolio.cash),
- "positions_count": len(self.portfolio.positions),
- "trades_count": len(self.portfolio.trades),
- "last_heartbeat": self._last_heartbeat,
- }
-
- def list_positions(self) -> List[Dict[str, Any]]:
- out = []
- for tk, p in self.portfolio.positions.items():
- last_px = float(self.portfolio.last_price.get(tk, p.entry_price))
- if p.side == "long":
- pnl = (last_px - p.entry_price) * p.size
- else:
- pnl = (p.entry_price - last_px) * p.size
- out.append({
- "ticker": tk,
- "side": p.side,
- "size": float(p.size),
- "entry_price": float(p.entry_price),
- "last_price": last_px,
- "pnl": float(pnl),
- "sl": p.sl,
- "tp": p.tp,
- })
- return out
-
- def list_trades(self) -> List[Dict[str, Any]]:
- return list(self.portfolio.trades)
-
- def list_equity(self) -> List[List[float]]:
- # [ [ts_ms, equity], ... ]
- return [[int(ts), float(eq)] for (ts, eq) in self.portfolio.portfolio_equity]
-
- # przyciski testowe z server.py
- def test_open_long(self, ticker: str, price: float, size: float):
- self.portfolio.last_price[ticker] = float(price)
- self.portfolio.open_long(ticker, float(size), float(price))
-
- def test_open_short(self, ticker: str, price: float, size: float):
- self.portfolio.last_price[ticker] = float(price)
- self.portfolio.open_short(ticker, float(size), float(price))
-
- def test_close(self, ticker: str, price: float | None):
- px = float(price) if price is not None else float(self.portfolio.last_price.get(ticker, 0.0))
- self.portfolio.close_all(ticker, px, reason="api:test_close")
-
- # ———————————————————— Core ————————————————————
-
- def _advance_index(self):
- if not self.syms:
- self._idx = 0
- return
- self._idx = (self._idx + 1) % len(self.syms)
-
- def _equity_now(self) -> float:
- eq = float(self.portfolio.cash)
- for p in self.portfolio.positions.values():
- px = float(self.portfolio.last_price.get(p.ticker, p.entry_price))
- if p.side == "long":
- eq += (px - p.entry_price) * p.size
- else:
- eq += (p.entry_price - px) * p.size
- return float(eq)
-
- def tick_once(self) -> float:
- t0 = time.time()
- self._round += 1
- if not self.syms:
- log.warning("No symbols configured")
- return 0.0
-
- tk = self.syms[self._idx % len(self.syms)]
- self._current_ticker = tk
- self._stage = "fetch"
- log.info("[ROUND %d] TICKER %s (%d/%d) — stage=fetch",
- self._round, tk, (self._idx % len(self.syms)) + 1, len(self.syms))
-
- try:
- # 1) POBIERZ DANE tylko dla jednego tickera
- batch = fetch_batch([tk], CFG.yf_period, CFG.interval)
- df = (batch or {}).get(tk)
- if df is None or len(df) == 0:
- log.warning("Data: %s -> EMPTY", tk)
- self._advance_index()
- return time.time() - t0
-
- # sanity kolumn
- if "Close" not in df.columns or df["Close"].dropna().empty:
- log.warning("Data: %s -> no usable Close column (cols=%s)", tk, list(df.columns))
- self._advance_index()
- return time.time() - t0
-
- last_close = float(pd.to_numeric(df["Close"], errors="coerce").dropna().iloc[-1])
- log.info("Data: %s -> bars=%d last_close=%.6f first=%s last=%s",
- tk, len(df), last_close, str(df.index[0]), str(df.index[-1]))
-
- # 2) AKTUALIZUJ HISTORIĘ
- if tk not in self.hist:
- self.hist[tk] = df.copy()
- else:
- append = df[df.index > self.hist[tk].index.max()]
- if not append.empty:
- self.hist[tk] = pd.concat([self.hist[tk], append])
-
- self.last_ts[tk] = df.index[-1]
- # aktualizuj „mark-to-market” dla equity
- self.portfolio.last_price[tk] = last_close
-
- # 3) SYGNAŁ
- self._stage = "signal"
- try:
- closes_preview = list(
- pd.to_numeric(self.hist[tk].get("Close"), errors="coerce").dropna().tail(3).round(6))
- except Exception:
- closes_preview = []
-
- decision: Decision = evaluate_signal(self.hist[tk])
- log.info("Signal: %s -> %s (last3=%s)", tk, decision, closes_preview)
-
- # 3.5) SIZING NA RYZYKO (1R = dystans do SL)
- size = CFG.min_size
- if decision.signal != "NONE" and decision.rpu > 0:
- equity = self._equity_now()
- risk_cash = max(0.0, equity) * CFG.risk_per_trade_frac
- size = risk_cash / decision.rpu
- size = max(CFG.min_size, min(CFG.max_size, size))
-
- # 4) EGZEKUCJA — TYLKO gdy BUY/SELL; NIE zamykamy przy NONE
- self._stage = "execute"
- action = "HOLD"
- if decision.signal == "BUY":
- self.portfolio.open_long(tk, size, last_close, sl=decision.sl, tp=decision.tp)
- action = "BUY"
- elif decision.signal == "SELL" and CFG.allow_short:
- self.portfolio.open_short(tk, size, last_close, sl=decision.sl, tp=decision.tp)
- action = "SELL"
-
- self._last_action = action
- log.info("Action: %s %s @ %.6f (size=%.2f)", action, tk, last_close, float(size))
-
- # 5) ZAPISZ WYJŚCIA (co rundę)
- self._stage = "save"
- # dociśnij próbkę equity dla UI (użyj timestampu ostatniej świecy)
- try:
- ts_ms = int(self.last_ts[tk].value / 1e6)
- self.portfolio.portfolio_equity.append((ts_ms, self._equity_now()))
- except Exception:
- # w ostateczności stempel teraz
- self.portfolio.portfolio_equity.append((int(time.time() * 1000), self._equity_now()))
-
- trades_df = pd.DataFrame(self.portfolio.trades)
- eq_df = pd.DataFrame(self.portfolio.portfolio_equity, columns=["time", "equity"])
- save_outputs(CFG.root_dir, self.syms, trades_df, eq_df, self.portfolio.cash)
- log.debug("Saved outputs | trades=%d equity=%d cash=%.2f",
- len(trades_df), len(eq_df), self.portfolio.cash)
-
- except Exception as e:
- log.exception("ERROR in tick_once(%s): %s", tk, e)
-
- finally:
- took = time.time() - t0
- self._last_heartbeat = time.time()
- self._stage = "sleep"
- self._advance_index()
- log.info("[ROUND %d] done in %.2fs | next index=%d", self._round, took, self._idx % len(self.syms))
- return took
diff --git a/new/trading_bot.py b/trading_bot.py
similarity index 100%
rename from new/trading_bot.py
rename to trading_bot.py