diff --git a/README.md b/README.md deleted file mode 100644 index d0780db..0000000 --- a/README.md +++ /dev/null @@ -1,226 +0,0 @@ -# Multi-Asset Portfolio Simulator (Yahoo Finance, Python) - -Szybki symulator wielo-aktywny z jedną wspólną kasą. -Działa w pętli co 2 minuty i dla paczki tickerów: - -- pobiera **batch** danych z Yahoo jednym zapytaniem (`yfinance.download`), -- **dopina tylko nowe świece** (cache ostatniego czasu per ticker), -- liczy sygnał na **close** każdej świecy, -- **egzekwuje** zlecenie na **kolejnym open** (bardziej realistycznie), -- prowadzi portfel (wspólne saldo, SL/TP intrabar, prowizja, poślizg), -- zapisuje wyniki do `dane/` oraz liczy metryki **MaxDD, Sharpe, CAGR**. - -> Uwaga: to **symulacja** oparta o dane z Yahoo. Nie składa prawdziwych zleceń. - ---- - -## Funkcje (skrót) - -- Multi-asset (~20 domyślnych: krypto + forex + złoto `GC=F`) -- Batch download (znacznie szybciej niż pojedynczo) -- Tylko nowe świece (bez pobierania pełnej historii co rundę) -- Sygnał: EMA200 + SMA(20/50) + RSI(14) + ATR-SL/TP + filtry: - **MACD, Stochastic, Bollinger, ADX, Supertrend** -- Egzekucja na **kolejnym OPEN** -- Portfel: **jedna** kasa dla wszystkich instrumentów, risk per trade (% equity) -- Zapisy CSV + **portfolio_summary** z MaxDD / Sharpe (annual) / CAGR - ---- - -## Wymagania - -- Python 3.9+ -- `pip install yfinance pandas numpy` - ---- - -## Szybki start - -```bash -# 1) instalacja -chmod +x install.sh -./install.sh - -# 2) uruchomienie (wirtualne środowisko) -source venv/bin/activate -python app.py - -# (opcjonalnie w tle) -nohup python app.py > output.log 2>&1 & -``` - -Logi lecą do `stdout` (lub do `output.log` przy `nohup`). - ---- - -## Struktura plików - -``` -project/ -├─ app.py # główna pętla: batch → nowe świece → decyzje → egzekucja → zapis -├─ config.py # konfiguracja (tickery, interwał, risk, katalog 'dane', itp.) -├─ data.py # fetch_batch(): pobieranie paczki tickerów z yfinance -├─ indicators.py # implementacje wskaźników (SMA/EMA/RSI/ATR/MACD/…) -├─ strategy.py # evaluate_signal(): logika generowania sygnałów + SL/TP + rpu -├─ portfolio.py # model portfela: pozycje, pending orders, SL/TP, PnL, equity -├─ metrics.py # metryki portfelowe: MaxDD, Sharpe (annualized), CAGR -├─ io_utils.py # zapisy do 'dane/': trades, equity, summary; tworzenie katalogów -├─ requirements.txt # zależności (yfinance, pandas, numpy) -└─ install.sh # instalator: venv + pip install -r requirements.txt -``` - -### Co jest w każdym pliku? - -- **`config.py`** - Definiuje `CFG` (tickers, interwał, okres pobierania `yf_period`, minimalna historia, risk, kasa startowa, SL/TP, katalog wyjściowy). - Zmienisz tu listę instrumentów lub parametry ryzyka. - -- **`data.py`** - `fetch_batch(tickers, period, interval)` — jedno zapytanie do Yahoo dla wielu tickerów. Zwraca słownik `{ticker: DataFrame}` z kolumnami `open, high, low, close, volume`. - -- **`indicators.py`** - Wskaźniki: `sma, ema, rsi, atr, macd, stoch_kd, bollinger, adx_val, supertrend`. - -- **`strategy.py`** - `evaluate_signal(df)` → `Decision(signal, sl, tp, rpu)` - - **signal**: `BUY` / `SELL` / `NONE` - - **sl/tp**: poziomy na bazie ATR i RR - - **rpu** (*risk per unit*): ile ryzyka na jednostkę (do wyliczenia wielkości pozycji) - -- **`portfolio.py`** - Model portfela z jedną kasą: - - egzekucja **na kolejnym OPEN** (pending orders), - - SL/TP intrabar, - - prowizja i poślizg, - - `portfolio_equity` (czas, equity) i lista `trades`. - -- **`metrics.py`** - Metryki portfela na bazie krzywej equity: - - **Max Drawdown** – minimalna wartość `equity/rolling_max - 1`, - - **Sharpe annualized** – z 1-min zwrotów (525 600 okresów/rok), - - **CAGR** – roczna złożona stopa wzrostu. - -- **`io_utils.py`** - Tworzy strukturę `dane/`, zapisuje: - - `dane/portfolio_equity.csv` – equity w czasie, - - `dane/portfolio_summary.txt` – JSON z metrykami i statystyką, - - `dane//_trades.csv` – dziennik zagrań per instrument. - -- **`app.py`** - Główna pętla: - 1) `fetch_batch` → nowe świeczki, - 2) dla każdego **nowego** bara: najpierw egzekucja pending na **open**, potem sygnał na **close** i zaplanowanie zlecenia na **kolejny open**, - 3) zapis CSV + metryki, - 4) pauza do pełnych 2 minut na rundę. - ---- - -## Struktura wyjściowa (w `dane/`) - -``` -dane/ -├─ portfolio_equity.csv # [time(ms), equity, datetime] -├─ portfolio_summary.txt # JSON z metrykami (MaxDD, Sharpe, CAGR, itp.) -├─ BTC-USD/ -│ └─ BTC-USD_trades.csv # dziennik transakcji dla BTC -├─ ETH-USD/ -│ └─ ETH-USD_trades.csv -└─ ... -``` - -**`*_trades.csv`** (kolumny): -`time, datetime, ticker, action(OPEN/CLOSE), side(long/short), price, size, pnl, reason, equity_after` - ---- - -## Konfiguracja (najczęściej zmieniane) - -Otwórz `config.py`: - -```python -CFG.tickers = ["BTC-USD","ETH-USD","EURUSD=X", ...] # Twoja lista -CFG.interval = "1m" # 1m / 2m / 5m / 15m / 30m / 60m / 1h -CFG.yf_period = "2d" # krótszy = szybciej; dopasuj do interwału -CFG.risk_per_trade = 0.005 # 0.5% equity na trade -CFG.starting_cash = 10000.0 # kasa startowa (w jednostce bazowej) -CFG.sl_atr_mult = 2.0 # ile ATR do SL -CFG.tp_rr = 1.5 # stosunek TP do ryzyka -CFG.root_dir = "dane" # katalog wyjściowy -``` - -> **Tip**: przy zmianie interwału ustaw odpowiednie `yf_period` (np. 1m→2d, 5m→10d, 1h→60d), żeby mieć min. ~250 barów do wskaźników. - ---- - -## Jak to działa (timeline 1 świecy) - -1. Nowy bar `t` przychodzi z Yahoo. -2. Jeśli był **pending order** zaplanowany na **bar `t`**, **otwiera się** na jego **OPEN**. -3. Jeśli jest otwarta pozycja, sprawdzamy **SL/TP intrabar** (`low/high`). -4. Na **CLOSE** bara `t` liczymy sygnał i **plan** (pending) na **OPEN** bara `t+1`. -5. Snapshot portfelowego equity po close. - ---- - -## Metryki - -- **MaxDD**: min wartość z `(equity/rolling_max - 1)`. -- **Sharpe (annualized)**: liczone z 1-min zwrotów `pct_change`, skalowane przez √(525 600). - (Brak stopy wolnej od ryzyka/rf ~ 0 w tej wersji.) -- **CAGR**: `((equity_end/equity_start)^(1/lata)) - 1`. - -> Uwaga: przy bardzo krótkich danych te metryki mogą być niestabilne. - ---- - -## FAQ - -**Q: Chcę inne interwały (np. 5m).** -A: Zmień `CFG.interval = "5m"` oraz `CFG.yf_period = "10d"` (żeby mieć ≥250 barów). - -**Q: Jak dodać/zmienić tickery?** -A: Zedytuj listę w `config.py`. Dla złota używamy `GC=F` (futures). - -**Q: Foldery per ticker nie powstają od razu.** -A: Tworzą się na starcie (`ensure_dirs`). Jeśli jakiś ticker nie ma danych z Yahoo, plik z transakcjami może nie powstać — ale katalog jest. - -**Q: Wolno działa?** -A: Ta wersja używa **jednego** zapytania na rundę + tylko **nowe** świece. Dalsze przyspieszenie: rozbij tickery na dwie paczki, jeśli Yahoo dławi się liczbą symboli. - -**Q: Jak uruchomić jako usługę systemd?** -A: -```ini -# /etc/systemd/system/portfolio.service -[Unit] -Description=Multi-Asset Portfolio Simulator -After=network.target - -[Service] -User=USER -WorkingDirectory=/ścieżka/do/project -ExecStart=/ścieżka/do/project/venv/bin/python /ścieżka/do/project/app.py -Restart=always - -[Install] -WantedBy=multi-user.target -``` -Potem: -```bash -sudo systemctl daemon-reload -sudo systemctl enable --now portfolio.service -journalctl -u portfolio.service -f -``` - ---- - -## Troubleshooting - -- **Yahoo 404 / brak danych dla symbolu** → Yahoo czasem zwraca puste dane. Po prostu pomija dany ticker w tej rundzie. -- **Za mało świec (history_min_bars)** → zwiększ `yf_period`. -- **Błędy czasu** → wymuszamy UTC; jeśli masz własne źródło, ujednolicaj strefę. -- **Sharpe = 0** → zbyt krótki okres lub zerowa zmienność w danych (rzadkie). - ---- - -## Licencja -Ten kod jest przykładowy/edukacyjny; używaj na własną odpowiedzialność. Nie stanowi porady inwestycyjnej. diff --git a/app.py b/app.py index 89c8574..1519601 100644 --- a/app.py +++ b/app.py @@ -1,76 +1,70 @@ from __future__ import annotations -import time -from typing import Dict -import pandas as pd -from config import CFG -from data import fetch_batch -from strategy import evaluate_signal -from portfolio import Portfolio -from io_utils import ensure_dirs, save_outputs +import os, json, threading +from flask import Flask, jsonify, render_template, abort +from trading_bot import main as trading_bot_main # dostosuj jeśli moduł nazywa się inaczej -def interval_to_timedelta(interval: str) -> pd.Timedelta: - # prosta mapka dla 1m/2m/5m/15m/30m/60m/1h - mapping = { - "1m":"1min","2m":"2min","5m":"5min","15m":"15min","30m":"30min", - "60m":"60min","90m":"90min","1h":"60min" - } - key = mapping.get(interval, "1min") - return pd.to_timedelta(key) +# Katalog projektu (tam gdzie leży ten plik) +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +# Domyślnie dane są w głównym katalogu projektu; można nadpisać przez env DATA_DIR +DATA_DIR = os.environ.get("DATA_DIR", BASE_DIR) + +def _load_json(name: str): + path = os.path.join(DATA_DIR, name) + if not os.path.isfile(path): + return None + try: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + return None + +app = Flask( + __name__, + static_folder="static", + static_url_path="/static", # <-- ważne: z wiodącym slashem + template_folder="templates", +) + +@app.route("/") +def index(): + return render_template("index.html") + +@app.route("/api/snapshot") +def api_snapshot(): + data = _load_json("snapshot.json") + if data is None: + abort(404, description="snapshot.json not found") + return jsonify(data) + +@app.route("/api/history") +def api_history(): + data = _load_json("portfolio_history.json") + if data is None: + abort(404, description="portfolio_history.json not found") + return jsonify(data) + +@app.route("/api/positions") +def api_positions(): + data = _load_json("positions.json") + if data is None: + data = [] + return jsonify(data) + +@app.route("/api/trades") +def api_trades(): + data = _load_json("trade_log.json") + if data is None: + data = [] + return jsonify(data) + +def start_trading_bot(): + """Uruchamia bota w osobnym wątku.""" + trading_thread = threading.Thread(target=trading_bot_main, daemon=True) + trading_thread.start() + print("[Flask] Trading bot uruchomiony w wątku.") if __name__ == "__main__": - ensure_dirs(CFG.root_dir, CFG.tickers) - portfolio = Portfolio(starting_cash=CFG.starting_cash, commission_per_trade=1.0, slippage_bp=1.0) - - last_ts: Dict[str, pd.Timestamp] = {} - hist: Dict[str, pd.DataFrame] = {} - bar_delta = interval_to_timedelta(CFG.interval) - - while True: - round_t0 = time.time() - batch = fetch_batch(CFG.tickers, CFG.yf_period, CFG.interval) - - for tk, df in batch.items(): - if df.empty: - continue - df = df.copy() - df.index = pd.to_datetime(df.index, utc=True) - - prev = hist.get(tk) - if prev is not None and not prev.empty: - df_all = pd.concat([prev, df[~df.index.isin(prev.index)]], axis=0).sort_index() - else: - df_all = df.sort_index() - hist[tk] = df_all.tail(2000) - - last = last_ts.get(tk) - new_part = df_all[df_all.index > last] if last is not None else df_all - - if not new_part.empty: - for ts, row in new_part.iterrows(): - o,h,l,c = float(row["open"]), float(row["high"]), float(row["low"]), float(row["close"]) - - # 1) egzekucja oczekujących na OPEN tego baraz - portfolio.on_new_bar(tk, ts, o,h,l,c) - - # 2) sygnał na CLOSE → plan na KOLEJNY OPEN - df_upto = hist[tk].loc[:ts] - dec = evaluate_signal(df_upto) - portfolio.schedule_order( - tk, dec.signal, dec.rpu, dec.sl, dec.tp, - next_bar_ts=ts + pd.to_timedelta(1, unit="min"), - ref_price=float(df_upto["close"].iloc[-1]) - ) - - last_ts[tk] = new_part.index[-1] - - # zapis - import pandas as pd - trades_df = pd.DataFrame(portfolio.trades) - eq_df = pd.DataFrame(portfolio.portfolio_equity, columns=["time","equity"]) - save_outputs(CFG.root_dir, CFG.tickers, trades_df, eq_df, portfolio.cash) - - elapsed = time.time() - round_t0 - sleep_s = max(0, 120 - elapsed) - print(f"Runda OK ({elapsed:.1f}s). Pauza {sleep_s:.1f}s.") - print(sleep_s) - time.sleep(sleep_s) + print(f"[Flask] DATA_DIR: {DATA_DIR}") + start_trading_bot() + app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)), debug=True) diff --git a/config.py b/config.py deleted file mode 100644 index c13491f..0000000 --- a/config.py +++ /dev/null @@ -1,57 +0,0 @@ -from dataclasses import dataclass, field -from typing import List - -@dataclass -class Settings: - # 20 FX - fx: List[str] = field(default_factory=lambda: [ - "EURUSD=X","GBPUSD=X","USDJPY=X","USDCHF=X","USDCAD=X", - "AUDUSD=X","NZDUSD=X","EURGBP=X","EURJPY=X","EURCHF=X", - "GBPJPY=X","AUDJPY=X","CHFJPY=X","CADJPY=X","EURAUD=X", - "EURNZD=X","GBPAUD=X","GBPCAD=X","AUDCAD=X","NZDJPY=X", - ]) - # 20 CRYPTO - crypto: List[str] = field(default_factory=lambda: [ - "BTC-USD","ETH-USD","BNB-USD","SOL-USD","XRP-USD", - "ADA-USD","DOGE-USD","TRX-USD","DOT-USD","AVAX-USD", - "MATIC-USD","LTC-USD","BCH-USD","LINK-USD","ATOM-USD", - "XMR-USD","XLM-USD","ETC-USD","FIL-USD","NEAR-USD", - ]) - - yf_period: str = "5d" - interval: str = "1m" - starting_cash: float = 10000.0 - loop_sleep_s: int = 2 - root_dir: str = "out" - commission_per_trade: float = 1.0 - slippage_bp: float = 1.0 - - # — parametry strategii — - history_min_bars: int = 220 - rsi_len: int = 14 - - # filtry – poluzowane - require_trend: bool = False # ignoruj EMA200 gdy False - use_macd_filter: bool = False # ignoruj MACD gdy False - adx_min: float = 8.0 - atr_min_frac_price: float = 1e-4 - rsi_buy_max: float = 75.0 - rsi_sell_min: float = 25.0 - - # SL/TP - sl_atr_mult: float = 1.3 - tp_rr: float = 1.5 - - # sizing na ryzyko - risk_per_trade_frac: float = 0.02 # 2% equity na 1R - min_size: float = 1.0 - max_size: float = 100000.0 - - # shorty - allow_short: bool = True - - @property - def tickers(self) -> List[str]: - return self.fx + self.crypto - -CFG = Settings() diff --git a/data.py b/data.py deleted file mode 100644 index 4ba3ff1..0000000 --- a/data.py +++ /dev/null @@ -1,84 +0,0 @@ -from __future__ import annotations -from typing import Dict, List -import time, logging -import pandas as pd -import yfinance as yf - -log = logging.getLogger("data") - -def _normalize_ohlc(df: pd.DataFrame) -> pd.DataFrame: - """Ujednolica kolumny do: Open, High, Low, Close, Volume. Obsługa lowercase, MultiIndex, Adj Close.""" - if df is None or len(df) == 0: - return pd.DataFrame(columns=["Open","High","Low","Close","Volume"]) - - df = df.copy() - - # Flatten MultiIndex -> zwykłe nazwy - if isinstance(df.columns, pd.MultiIndex): - df.columns = [str(tuple(filter(None, map(str, c)))).strip("()").replace("'", "").replace(" ", "") if isinstance(c, tuple) else str(c) for c in df.columns] - # po flatten często nazwy są np. 'Close,EURUSD=X' – weź pierwszy człon przed przecinkiem - df.columns = [c.split(",")[0] for c in df.columns] - - # Zrzuć TZ - if isinstance(df.index, pd.DatetimeIndex) and df.index.tz is not None: - df.index = df.index.tz_localize(None) - - # Ujednolicenie do TitleCase - norm = {c: str(c).strip() for c in df.columns} - # mapuj najczęstsze warianty - mapping = {} - for c in norm.values(): - lc = c.lower() - if lc in ("open", "op", "o"): mapping[c] = "Open" - elif lc in ("high", "hi", "h"): mapping[c] = "High" - elif lc in ("low", "lo", "l"): mapping[c] = "Low" - elif lc in ("close", "cl", "c"): mapping[c] = "Close" - elif lc in ("adj close","adjclose","adjustedclose"): mapping[c] = "Adj Close" - elif lc in ("volume","vol","v"): mapping[c] = "Volume" - else: - # zostaw jak jest (np. 'Dividends', 'Stock Splits') - mapping[c] = c - - df.rename(columns=mapping, inplace=True) - - # Jeśli brak Close, ale jest Adj Close -> użyj go - if "Close" not in df.columns and "Adj Close" in df.columns: - df["Close"] = df["Adj Close"] - - # Upewnij się, że są wszystkie podstawowe kolumny (dodaj puste jeśli brak) - for need in ["Open","High","Low","Close","Volume"]: - if need not in df.columns: - df[need] = pd.NA - - # Pozostaw tylko rdzeń (kolejność stała) - df = df[["Open","High","Low","Close","Volume"]] - return df - -def _fetch_single(ticker: str, period: str, interval: str, tries: int = 3, sleep_s: float = 0.7) -> pd.DataFrame: - """Pobierz OHLC dla 1 tickera (z retry) i znormalizuj kolumny.""" - for i in range(1, tries + 1): - try: - log.info("Yahoo: get %s (try %d/%d) period=%s interval=%s", ticker, i, tries, period, interval) - df = yf.Ticker(ticker).history(period=period, interval=interval, auto_adjust=False, prepost=False) - df = _normalize_ohlc(df) - if len(df) == 0 or "Close" not in df.columns: - log.warning("Yahoo: %s -> EMPTY or no Close (cols=%s)", ticker, list(df.columns)) - return df - except Exception as e: - log.warning("Yahoo: error %s (try %d/%d): %s", ticker, i, tries, e) - time.sleep(sleep_s) - # po niepowodzeniu zwróć pusty rdzeń - return pd.DataFrame(columns=["Open","High","Low","Close","Volume"]) - -def fetch_batch(tickers: List[str], period: str, interval: str) -> Dict[str, pd.DataFrame]: - """Pobierz paczkę danych dla listy tickerów.""" - out: Dict[str, pd.DataFrame] = {} - for tk in tickers: - df = _fetch_single(tk, period, interval) - out[tk] = df - if len(df): - first = str(df.index[0]); last = str(df.index[-1]) - else: - first = last = "-" - log.info("Yahoo: %s -> bars=%d %s..%s cols=%s", tk, len(df), first, last, list(df.columns)) - return out diff --git a/indicators.py b/indicators.py index 0cc2e1c..6ebd7e4 100644 --- a/indicators.py +++ b/indicators.py @@ -1,33 +1,50 @@ +# indicators.py from __future__ import annotations import math -from typing import Tuple import numpy as np import pandas as pd +# ===== Helper ===== +def _as_1d(a) -> np.ndarray: + return np.asarray(a, dtype=float).ravel() + +# ===== Indicators ===== def sma(arr: np.ndarray, period: int) -> np.ndarray: - if len(arr) < period: return np.full(len(arr), np.nan) + arr = _as_1d(arr); n = len(arr) + if n == 0: return np.array([], float) + if n < period: return np.full(n, np.nan) w = np.ones(period) / period out = np.convolve(arr, w, mode="valid") return np.concatenate([np.full(period-1, np.nan), out]) def ema(arr: np.ndarray, period: int) -> np.ndarray: - out = np.full(len(arr), np.nan, dtype=float); k = 2/(period+1); e = np.nan - for i,x in enumerate(arr): - e = x if math.isnan(e) else x*k + e*(1-k) + arr = _as_1d(arr); n = len(arr) + out = np.full(n, np.nan, float) + if n == 0: return out + k = 2/(period+1); e = np.nan + for i, x in enumerate(arr): + e = x if (isinstance(e, float) and math.isnan(e)) else x*k + e*(1-k) out[i] = e return out -def rsi(arr: np.ndarray, period: int=14) -> np.ndarray: - if len(arr) < period+1: return np.full(len(arr), np.nan) - d = np.diff(arr, prepend=arr[0]); g = np.where(d>0,d,0.0); L = np.where(d<0,-d,0.0) - ag, al = ema(g,period), ema(L,period); rs = ag/(al+1e-9) +def rsi(arr: np.ndarray, period: int = 14) -> np.ndarray: + arr = _as_1d(arr); n = len(arr) + if n == 0: return np.array([], float) + if n < period+1: return np.full(n, np.nan) + d = np.diff(arr, prepend=arr[0]) + g = np.where(d > 0, d, 0.0) + L = np.where(d < 0, -d, 0.0) + ag, al = ema(g, period), ema(L, period) + rs = ag / (al + 1e-9) return 100 - (100 / (1 + rs)) -def atr(H: np.ndarray, L: np.ndarray, C: np.ndarray, period: int=14) -> np.ndarray: - if len(C) < period+1: return np.full(len(C), np.nan) - pc = np.roll(C,1) - tr = np.maximum.reduce([H-L, np.abs(H-pc), np.abs(L-pc)]) - tr[0] = H[0]-L[0] +def atr(H: np.ndarray, L: np.ndarray, C: np.ndarray, period: int = 14) -> np.ndarray: + H, L, C = _as_1d(H), _as_1d(L), _as_1d(C); n = len(C) + if n == 0: return np.array([], float) + if n < period+1: return np.full(n, np.nan) + pc = np.roll(C, 1) + tr = np.maximum.reduce([H - L, np.abs(H - pc), np.abs(L - pc)]) + tr[0] = H[0] - L[0] return ema(tr, period) def macd(arr: np.ndarray, fast=12, slow=26, signal=9): @@ -38,7 +55,11 @@ def macd(arr: np.ndarray, fast=12, slow=26, signal=9): return line, sig, hist def stoch_kd(H: np.ndarray, L: np.ndarray, C: np.ndarray, period=14, smooth=3): - if len(C) < period: return np.full(len(C), np.nan), np.full(len(C), np.nan) + H, L, C = _as_1d(H), _as_1d(L), _as_1d(C); n = len(C) + if n == 0: + z = np.array([], float); return z, z + if n < period: + return np.full(n, np.nan), np.full(n, np.nan) lowest = pd.Series(L).rolling(period, min_periods=1).min().to_numpy() highest = pd.Series(H).rolling(period, min_periods=1).max().to_numpy() k = 100 * (C - lowest) / (highest - lowest + 1e-9) @@ -46,31 +67,59 @@ def stoch_kd(H: np.ndarray, L: np.ndarray, C: np.ndarray, period=14, smooth=3): return k, d def bollinger(arr: np.ndarray, period=20, dev=2.0): + arr = _as_1d(arr); n = len(arr) + if n == 0: + z = np.array([], float); return z, z, z s = pd.Series(arr) - ma = s.rollizng(period, min_periods=1).mean().to_numpy() + ma = s.rolling(period, min_periods=1).mean().to_numpy() sd = s.rolling(period, min_periods=1).std(ddof=0).to_numpy() return ma, ma + dev*sd, ma - dev*sd def adx_val(H: np.ndarray, L: np.ndarray, C: np.ndarray, period=14): - up_move = H - np.roll(H,1); down_move = np.roll(L,1) - L + H, L, C = _as_1d(H), _as_1d(L), _as_1d(C); n = len(C) + if n == 0: + z = np.array([], float); return z, z, z + up_move = H - np.roll(H, 1); down_move = np.roll(L, 1) - L up_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0.0) down_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0.0) - tr1 = H - L; tr2 = np.abs(H - np.roll(C,1)); tr3 = np.abs(L - np.roll(C,1)) + tr1 = H - L; tr2 = np.abs(H - np.roll(C, 1)); tr3 = np.abs(L - np.roll(C, 1)) tr = np.maximum.reduce([tr1, tr2, tr3]); tr[0] = tr1[0] atr14 = ema(tr, period) - pdi = 100 * ema(up_dm, period)/(atr14+1e-9) - mdi = 100 * ema(down_dm, period)/(atr14+1e-9) - dx = 100 * np.abs(pdi - mdi)/(pdi + mdi + 1e-9) + pdi = 100 * ema(up_dm, period) / (atr14 + 1e-9) + mdi = 100 * ema(down_dm, period) / (atr14 + 1e-9) + dx = 100 * np.abs(pdi - mdi) / (pdi + mdi + 1e-9) adx = ema(dx, period) return adx, pdi, mdi def supertrend(H: np.ndarray, L: np.ndarray, C: np.ndarray, period=10, mult=3.0): + H, L, C = _as_1d(H), _as_1d(L), _as_1d(C); n = len(C) + if n == 0: return np.array([], float) a = atr(H, L, C, period).copy() - hl2 = (H + L)/2.0 - ub = hl2 + mult*a - lb = hl2 - mult*a - n = len(C); st = np.full(n, np.nan) + hl2 = (H + L) / 2.0 + ub = hl2 + mult * a + lb = hl2 - mult * a + st = np.full(n, np.nan) for i in range(1, n): prev = st[i-1] if not np.isnan(st[i-1]) else hl2[i-1] st[i] = ub[i] if C[i-1] <= prev else lb[i] return st + +# ===== Pipeline: add_indicators ===== +def add_indicators(df: pd.DataFrame, min_bars: int = 200) -> pd.DataFrame: + if len(df) < min_bars: + raise ValueError(f"Too few bars: {len(df)} < {min_bars}") + C, H, L = df["close"].to_numpy(), df["high"].to_numpy(), df["low"].to_numpy() + df["ema20"] = ema(C, 20) + df["ema50"] = ema(C, 50) + df["rsi14"] = rsi(C, 14) + df["atr14"] = atr(H, L, C, 14) + m_line, m_sig, _ = macd(C) + df["macd"], df["macd_sig"] = m_line, m_sig + k, d = stoch_kd(H, L, C, 14, 3) + df["stoch_k"], df["stoch_d"] = k, d + mid, up, dn = bollinger(C, 20, 2.0) + df["bb_mid"], df["bb_up"], df["bb_dn"] = mid, up, dn + adx_, pdi, mdi = adx_val(H, L, C, 14) + df["adx"], df["pdi"], df["mdi"] = adx_, pdi, mdi + df["supertrend"] = supertrend(H, L, C, 10, 3.0) + return df diff --git a/install.sh b/install.sh deleted file mode 100644 index b9b85b4..0000000 --- a/install.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -e -python3 -m venv venv -source venv/bin/activate -pip install --upgrade pip -pip install -r requirements.txt -echo "OK. Start:" -echo "source venv/bin/activate && python app.py" - - -#chmod +x install.sh -#./install.sh -#source venv/bin/activate -#nohup python app.py > output.log 2>&1 & \ No newline at end of file diff --git a/io_utils.py b/io_utils.py deleted file mode 100644 index 3f7a457..0000000 --- a/io_utils.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import annotations -import os, json -import pandas as pd -from typing import List -from metrics import portfolio_metrics - -def ensure_dirs(root_dir: str, tickers: List[str]): - os.makedirs(root_dir, exist_ok=True) - for tk in tickers: - os.makedirs(os.path.join(root_dir, tk), exist_ok=True) - -def save_outputs(root_dir: str, tickers: List[str], trades: pd.DataFrame, portfolio_eq: pd.DataFrame, cash: float): - ensure_dirs(root_dir, tickers) - - if not trades.empty: - for tk, tdf in trades.groupby("ticker"): - tdf2 = tdf.copy() - tdf2["datetime"] = pd.to_datetime(tdf2["time"], unit="ms", utc=True) - tdf2.to_csv(os.path.join(root_dir, tk, f"{tk}_trades.csv"), index=False) - - if not portfolio_eq.empty: - portfolio_eq["datetime"] = pd.to_datetime(portfolio_eq["time"], unit="ms", utc=True) - portfolio_eq.to_csv(os.path.join(root_dir, "portfolio_equity.csv"), index=False) - - eq_series = portfolio_eq.set_index("time")["equity"] if not portfolio_eq.empty else pd.Series(dtype=float) - metrics = portfolio_metrics(eq_series) if not eq_series.empty else {"max_dd":0.0,"sharpe":0.0,"cagr":0.0} - - summary = { - "ending_cash": round(cash, 2), - "n_open_positions": int(trades[trades["action"]=="OPEN"]["ticker"].nunique()) if not trades.empty else 0, - "n_trades_closed": int((trades["action"]=="CLOSE").sum()) if not trades.empty else 0, - "max_drawdown": metrics["max_dd"], - "sharpe_annualized": metrics["sharpe"], - "cagr": metrics["cagr"], - } - with open(os.path.join(root_dir, "portfolio_summary.txt"), "w", encoding="utf-8") as f: - json.dump(summary, f, indent=2, ensure_ascii=False) diff --git a/metrics.py b/metrics.py deleted file mode 100644 index 7b55542..0000000 --- a/metrics.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations -import math -import pandas as pd -from typing import Dict - -def portfolio_metrics(equity: pd.Series) -> Dict[str, float]: - if equity.empty or equity.iloc[0] <= 0: - return {"max_dd": 0.0, "sharpe": 0.0, "cagr": 0.0} - - roll_max = equity.cummax() - dd = (equity/roll_max) - 1.0 - max_dd = float(dd.min()) - - rets = equity.pct_change().dropna() - if rets.empty or rets.std(ddof=0) == 0: - sharpe = 0.0 - else: - periods_per_year = 365*24*60 # dla 1m - sharpe = float((rets.mean()/rets.std(ddof=0)) * math.sqrt(periods_per_year)) - - t0 = pd.to_datetime(equity.index[0], unit="ms", utc=True) - t1 = pd.to_datetime(equity.index[-1], unit="ms", utc=True) - years = max((t1 - t0).total_seconds() / (365.25*24*3600), 1e-9) - cagr = float((equity.iloc[-1]/equity.iloc[0])**(1/years) - 1.0) - - return {"max_dd": max_dd, "sharpe": sharpe, "cagr": cagr} diff --git a/new/app.py b/new/app.py deleted file mode 100644 index b855b43..0000000 --- a/new/app.py +++ /dev/null @@ -1,61 +0,0 @@ -from __future__ import annotations -import os, json, threading -from flask import Flask, jsonify, render_template, abort -from trading_bot import main as trading_bot_main # zakładam, że tak nazywasz plik - -DATA_DIR = os.environ.get("DATA_DIR", ".") - -def _load_json(name: str): - path = os.path.join(DATA_DIR, name) - if not os.path.isfile(path): - return None - with open(path, "r", encoding="utf-8") as f: - try: - return json.load(f) - except Exception: - return None - -app = Flask( - __name__, - static_folder="static", - static_url_path="/static", - template_folder="templates" -) - -@app.route("/") -def index(): - return render_template("index.html") - -@app.route("/api/snapshot") -def api_snapshot(): - data = _load_json("snapshot.json") - if data is None: abort(404, "snapshot.json not found") - return jsonify(data) - -@app.route("/api/history") -def api_history(): - data = _load_json("portfolio_history.json") - if data is None: abort(404, "portfolio_history.json not found") - return jsonify(data) - -@app.route("/api/positions") -def api_positions(): - data = _load_json("positions.json") - if data is None: data = [] - return jsonify(data) - -@app.route("/api/trades") -def api_trades(): - data = _load_json("trade_log.json") - if data is None: data = [] - return jsonify(data) - -def start_trading_bot(): - """Uruchamia bota w osobnym wątku.""" - trading_thread = threading.Thread(target=trading_bot_main, daemon=True) - trading_thread.start() - print("[Flask] Trading bot uruchomiony w wątku.") - -if __name__ == "__main__": - start_trading_bot() - app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)), debug=True) diff --git a/new/indicators.py b/new/indicators.py deleted file mode 100644 index 6ebd7e4..0000000 --- a/new/indicators.py +++ /dev/null @@ -1,125 +0,0 @@ -# indicators.py -from __future__ import annotations -import math -import numpy as np -import pandas as pd - -# ===== Helper ===== -def _as_1d(a) -> np.ndarray: - return np.asarray(a, dtype=float).ravel() - -# ===== Indicators ===== -def sma(arr: np.ndarray, period: int) -> np.ndarray: - arr = _as_1d(arr); n = len(arr) - if n == 0: return np.array([], float) - if n < period: return np.full(n, np.nan) - w = np.ones(period) / period - out = np.convolve(arr, w, mode="valid") - return np.concatenate([np.full(period-1, np.nan), out]) - -def ema(arr: np.ndarray, period: int) -> np.ndarray: - arr = _as_1d(arr); n = len(arr) - out = np.full(n, np.nan, float) - if n == 0: return out - k = 2/(period+1); e = np.nan - for i, x in enumerate(arr): - e = x if (isinstance(e, float) and math.isnan(e)) else x*k + e*(1-k) - out[i] = e - return out - -def rsi(arr: np.ndarray, period: int = 14) -> np.ndarray: - arr = _as_1d(arr); n = len(arr) - if n == 0: return np.array([], float) - if n < period+1: return np.full(n, np.nan) - d = np.diff(arr, prepend=arr[0]) - g = np.where(d > 0, d, 0.0) - L = np.where(d < 0, -d, 0.0) - ag, al = ema(g, period), ema(L, period) - rs = ag / (al + 1e-9) - return 100 - (100 / (1 + rs)) - -def atr(H: np.ndarray, L: np.ndarray, C: np.ndarray, period: int = 14) -> np.ndarray: - H, L, C = _as_1d(H), _as_1d(L), _as_1d(C); n = len(C) - if n == 0: return np.array([], float) - if n < period+1: return np.full(n, np.nan) - pc = np.roll(C, 1) - tr = np.maximum.reduce([H - L, np.abs(H - pc), np.abs(L - pc)]) - tr[0] = H[0] - L[0] - return ema(tr, period) - -def macd(arr: np.ndarray, fast=12, slow=26, signal=9): - ef, es = ema(arr, fast), ema(arr, slow) - line = ef - es - sig = ema(line, signal) - hist = line - sig - return line, sig, hist - -def stoch_kd(H: np.ndarray, L: np.ndarray, C: np.ndarray, period=14, smooth=3): - H, L, C = _as_1d(H), _as_1d(L), _as_1d(C); n = len(C) - if n == 0: - z = np.array([], float); return z, z - if n < period: - return np.full(n, np.nan), np.full(n, np.nan) - lowest = pd.Series(L).rolling(period, min_periods=1).min().to_numpy() - highest = pd.Series(H).rolling(period, min_periods=1).max().to_numpy() - k = 100 * (C - lowest) / (highest - lowest + 1e-9) - d = pd.Series(k).rolling(smooth, min_periods=1).mean().to_numpy() - return k, d - -def bollinger(arr: np.ndarray, period=20, dev=2.0): - arr = _as_1d(arr); n = len(arr) - if n == 0: - z = np.array([], float); return z, z, z - s = pd.Series(arr) - ma = s.rolling(period, min_periods=1).mean().to_numpy() - sd = s.rolling(period, min_periods=1).std(ddof=0).to_numpy() - return ma, ma + dev*sd, ma - dev*sd - -def adx_val(H: np.ndarray, L: np.ndarray, C: np.ndarray, period=14): - H, L, C = _as_1d(H), _as_1d(L), _as_1d(C); n = len(C) - if n == 0: - z = np.array([], float); return z, z, z - up_move = H - np.roll(H, 1); down_move = np.roll(L, 1) - L - up_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0.0) - down_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0.0) - tr1 = H - L; tr2 = np.abs(H - np.roll(C, 1)); tr3 = np.abs(L - np.roll(C, 1)) - tr = np.maximum.reduce([tr1, tr2, tr3]); tr[0] = tr1[0] - atr14 = ema(tr, period) - pdi = 100 * ema(up_dm, period) / (atr14 + 1e-9) - mdi = 100 * ema(down_dm, period) / (atr14 + 1e-9) - dx = 100 * np.abs(pdi - mdi) / (pdi + mdi + 1e-9) - adx = ema(dx, period) - return adx, pdi, mdi - -def supertrend(H: np.ndarray, L: np.ndarray, C: np.ndarray, period=10, mult=3.0): - H, L, C = _as_1d(H), _as_1d(L), _as_1d(C); n = len(C) - if n == 0: return np.array([], float) - a = atr(H, L, C, period).copy() - hl2 = (H + L) / 2.0 - ub = hl2 + mult * a - lb = hl2 - mult * a - st = np.full(n, np.nan) - for i in range(1, n): - prev = st[i-1] if not np.isnan(st[i-1]) else hl2[i-1] - st[i] = ub[i] if C[i-1] <= prev else lb[i] - return st - -# ===== Pipeline: add_indicators ===== -def add_indicators(df: pd.DataFrame, min_bars: int = 200) -> pd.DataFrame: - if len(df) < min_bars: - raise ValueError(f"Too few bars: {len(df)} < {min_bars}") - C, H, L = df["close"].to_numpy(), df["high"].to_numpy(), df["low"].to_numpy() - df["ema20"] = ema(C, 20) - df["ema50"] = ema(C, 50) - df["rsi14"] = rsi(C, 14) - df["atr14"] = atr(H, L, C, 14) - m_line, m_sig, _ = macd(C) - df["macd"], df["macd_sig"] = m_line, m_sig - k, d = stoch_kd(H, L, C, 14, 3) - df["stoch_k"], df["stoch_d"] = k, d - mid, up, dn = bollinger(C, 20, 2.0) - df["bb_mid"], df["bb_up"], df["bb_dn"] = mid, up, dn - adx_, pdi, mdi = adx_val(H, L, C, 14) - df["adx"], df["pdi"], df["mdi"] = adx_, pdi, mdi - df["supertrend"] = supertrend(H, L, C, 10, 3.0) - return df diff --git a/new/new.zip b/new/new.zip deleted file mode 100644 index 125111e..0000000 Binary files a/new/new.zip and /dev/null differ 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 - - - -
-

Trading Dashboard

-
- Ostatnia aktualizacja: — - -
-
- -
- -
-
-
Gotówka
-
-
-
-
Wartość konta (total)
-
-
-
-
Zysk (otwarte pozycje)
-
-
-
-
Otwarte pozycje
-
-
-
- - -
-

Wartość konta (total) — wykres

-
- -
-
- - -
-

Gotówka — wykres

-
- -
-
- - -
-

Otwarte pozycje

- - - - - - - - - - - - - -
TickerQtyEntrySideLast priceUnreal. PnLUnreal. PnL %
-
- - -
-

Ostatnie sygnały

- - - - - - - - - - - -
TickerTimePriceSignalInterval
-
- - -
-

Transakcje (ostatnie 50)

- - - - - - - - - - - - - - -
TimeActionTickerPriceQtyPnLPnL %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 9bf7ff5..0000000 Binary files a/pythonProject.zip and /dev/null differ diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 2ab18e8..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -yfinance -pandas -numpy -Flask \ No newline at end of file diff --git a/server.py b/server.py deleted file mode 100644 index 3a1d3b3..0000000 --- a/server.py +++ /dev/null @@ -1,83 +0,0 @@ -from __future__ import annotations -import logging -from flask import Flask, jsonify, render_template, request -from trader import TraderWorker - -logging.basicConfig( - level=logging.DEBUG, - format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s" -) -log = logging.getLogger("server") - -app = Flask(__name__, template_folder="templates", static_folder="static") -worker = TraderWorker() - -@app.before_request -def _log_request(): - log.debug("REQ %s %s args=%s json=%s", - request.method, request.path, dict(request.args), request.get_json(silent=True)) - -@app.after_request -def _after(resp): - resp.headers["Cache-Control"] = "no-store, max-age=0" - resp.headers["Pragma"] = "no-cache" - resp.headers["Expires"] = "0" - log.debug("RESP %s %s %s", request.method, request.path, resp.status) - return resp - -@app.get("/") -def index(): - return render_template("index.html") - -@app.get("/api/status") -def api_status(): - return jsonify(worker.status()) - -@app.get("/api/positions") -def api_positions(): - return jsonify({"positions": worker.list_positions()}) - -@app.get("/api/trades") -def api_trades(): - return jsonify({"trades": worker.list_trades()}) - -@app.get("/api/equity") -def api_equity(): - return jsonify({"equity": worker.list_equity()}) - -@app.post("/api/start") -def api_start(): - ok = worker.start() - return jsonify({"started": ok, "running": worker.is_running()}) - -@app.post("/api/stop") -def api_stop(): - ok = worker.stop() - return jsonify({"stopped": ok, "running": worker.is_running()}) - -@app.post("/api/run-once") -def api_run_once(): - took = worker.tick_once() - return jsonify({"ok": True, "took_s": took}) - -# testowe (opcjonalne) -@app.post("/api/test/long") -def api_test_long(): - data = request.get_json(silent=True) or {} - worker.test_open_long(data.get("ticker","AAPL"), data.get("price",123.45), data.get("size",1.0)) - return jsonify({"ok": True}) - -@app.post("/api/test/short") -def api_test_short(): - data = request.get_json(silent=True) or {} - worker.test_open_short(data.get("ticker","AAPL"), data.get("price",123.45), data.get("size",1.0)) - return jsonify({"ok": True}) - -@app.post("/api/test/close") -def api_test_close(): - data = request.get_json(silent=True) or {} - worker.test_close(data.get("ticker","AAPL"), data.get("price")) - return jsonify({"ok": True}) - -if __name__ == "__main__": - app.run(host="0.0.0.0", port=8000, debug=False) diff --git a/new/signals_scan.json b/signals_scan.json similarity index 60% rename from new/signals_scan.json rename to signals_scan.json index 23a5472..7230fc8 100644 --- a/new/signals_scan.json +++ b/signals_scan.json @@ -1,71 +1,8 @@ [ - { - "ticker": "GBPUSD=X", - "time": "2025-08-15 10:17:00+00:00", - "price": 1.3561896085739136, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, { "ticker": "EURUSD=X", - "time": "2025-08-15 10:17:00+00:00", - "price": 1.1691803932189941, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "CHFJPY=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 182.3179931640625, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "DOGE-USD", - "time": "2025-08-15 10:15:00+00:00", - "price": 0.2317201942205429, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "OP-USD", - "time": "2025-08-15 10:11:00+00:00", - "price": 0.7549265623092651, - "signal": -1, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "BTC-USD", - "time": "2025-08-15 10:15:00+00:00", - "price": 118967.421875, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "NZDUSD=X", - "time": "2025-08-15 10:16:00+00:00", - "price": 0.5927330851554871, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "EURAUD=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 1.7958300113677979, + "time": "2025-08-15 10:30:00+00:00", + "price": 1.1689070463180542, "signal": 0, "period": "7d", "interval": "1m", @@ -73,215 +10,8 @@ }, { "ticker": "USDCHF=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 0.8054800033569336, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "SOL-USD", - "time": "2025-08-15 10:15:00+00:00", - "price": 195.40365600585938, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "USDJPY=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 146.86399841308594, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "GBPJPY=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 199.14500427246094, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "XLM-USD", - "time": "2025-08-15 10:16:00+00:00", - "price": 0.42842021584510803, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "EURGBP=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 0.8619400262832642, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "LTC-USD", - "time": "2025-08-15 10:14:00+00:00", - "price": 121.15275573730469, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "USDCAD=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 1.3791099786758423, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "BNB-USD", - "time": "2025-08-15 10:15:00+00:00", - "price": 844.4680786132812, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "ETH-USD", - "time": "2025-08-15 10:16:00+00:00", - "price": 4635.28955078125, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "ETC-USD", - "time": "2025-08-15 10:15:00+00:00", - "price": 22.40416717529297, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "CADJPY=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 106.49600219726562, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "TRX-USD", - "time": "2025-08-15 10:16:00+00:00", - "price": 0.3588825762271881, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "EURJPY=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 171.65899658203125, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "TON-USD", - "time": "2025-08-15 10:11:00+00:00", - "price": 0.01708882860839367, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "ADA-USD", - "time": "2025-08-15 10:16:00+00:00", - "price": 0.951282799243927, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "NZDJPY=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 87.01699829101562, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "AUDJPY=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 95.58399963378906, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "EURCAD=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 1.611799955368042, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "BCH-USD", - "time": "2025-08-15 10:15:00+00:00", - "price": 596.3115844726562, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "DOT-USD", - "time": "2025-08-15 10:15:00+00:00", - "price": 4.005911827087402, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "ATOM-USD", - "time": "2025-08-15 10:16:00+00:00", - "price": 4.517627239227295, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "AUDNZD=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 1.0983599424362183, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "GC=F", - "time": "2025-08-15 10:08:00+00:00", - "price": 3386.10009765625, + "time": "2025-08-15 10:31:00+00:00", + "price": 0.8058599829673767, "signal": 0, "period": "7d", "interval": "1m", @@ -289,44 +19,35 @@ }, { "ticker": "NEAR-USD", - "time": "2025-08-15 10:11:00+00:00", - "price": 2.7867467403411865, - "signal": -1, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "GBPCAD=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 1.8698300123214722, + "time": "2025-08-15 10:21:00+00:00", + "price": 2.7824549674987793, "signal": 0, "period": "7d", "interval": "1m", "error": null }, { - "ticker": "XRP-USD", - "time": "2025-08-15 10:16:00+00:00", - "price": 3.111118793487549, + "ticker": "USDJPY=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 146.8820037841797, "signal": 0, "period": "7d", "interval": "1m", "error": null }, { - "ticker": "EURCHF=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 0.9409899711608887, + "ticker": "ETH-USD", + "time": "2025-08-15 10:28:00+00:00", + "price": 4645.15771484375, "signal": 0, "period": "7d", "interval": "1m", "error": null }, { - "ticker": "LINK-USD", - "time": "2025-08-15 10:16:00+00:00", - "price": 22.371389389038086, + "ticker": "CADJPY=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 106.47799682617188, "signal": 0, "period": "7d", "interval": "1m", @@ -334,8 +55,287 @@ }, { "ticker": "AUDUSD=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 0.6509992480278015, + "time": "2025-08-15 10:31:00+00:00", + "price": 0.6507663130760193, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "OP-USD", + "time": "2025-08-15 10:26:00+00:00", + "price": 0.7553843259811401, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "EURJPY=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 171.62600708007812, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "NZDUSD=X", + "time": "2025-08-15 10:30:00+00:00", + "price": 0.5924872159957886, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "ETC-USD", + "time": "2025-08-15 10:30:00+00:00", + "price": 22.407447814941406, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "GC=F", + "time": "2025-08-15 10:21:00+00:00", + "price": 3386.89990234375, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "GBPJPY=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 199.0590057373047, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "AUDJPY=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 95.55599975585938, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "TRX-USD", + "time": "2025-08-15 10:29:00+00:00", + "price": 0.35983800888061523, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "TON-USD", + "time": "2025-08-15 10:26:00+00:00", + "price": 0.01708882860839367, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "LTC-USD", + "time": "2025-08-15 10:29:00+00:00", + "price": 121.18510437011719, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "EURCAD=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 1.6117500066757202, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "AUDNZD=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 1.0984100103378296, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "USDCAD=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 1.3794000148773193, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "ATOM-USD", + "time": "2025-08-15 10:29:00+00:00", + "price": 4.5298662185668945, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "BCH-USD", + "time": "2025-08-15 10:30:00+00:00", + "price": 595.4796142578125, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "LINK-USD", + "time": "2025-08-15 10:29:00+00:00", + "price": 22.34357452392578, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "XRP-USD", + "time": "2025-08-15 10:29:00+00:00", + "price": 3.117891550064087, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "DOT-USD", + "time": "2025-08-15 10:30:00+00:00", + "price": 4.020590305328369, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "BTC-USD", + "time": "2025-08-15 10:29:00+00:00", + "price": 119062.5703125, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "GBPUSD=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 1.3552889823913574, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "CHFJPY=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 182.2550048828125, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "ADA-USD", + "time": "2025-08-15 10:30:00+00:00", + "price": 0.9589924216270447, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "XLM-USD", + "time": "2025-08-15 10:29:00+00:00", + "price": 0.42904016375541687, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "EURCHF=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 0.9415299892425537, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "BNB-USD", + "time": "2025-08-15 10:30:00+00:00", + "price": 845.3945922851562, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "DOGE-USD", + "time": "2025-08-15 10:30:00+00:00", + "price": 0.23236910998821259, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "NZDJPY=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 86.98799896240234, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "EURGBP=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 0.8621399998664856, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "SOL-USD", + "time": "2025-08-15 10:29:00+00:00", + "price": 195.72109985351562, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "EURAUD=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 1.7960000038146973, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "GBPCAD=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 1.869420051574707, "signal": 0, "period": "7d", "interval": "1m", diff --git a/new/snapshot.json b/snapshot.json similarity index 55% rename from new/snapshot.json rename to snapshot.json index f97cac6..2d48ed8 100644 --- a/new/snapshot.json +++ b/snapshot.json @@ -1,110 +1,22 @@ { - "time": "2025-08-15 12:19:02", + "time": "2025-08-15 12:32:18", "last_history": { - "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, + "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": 0.0, - "open_positions": 2 + "realized_pnl_cum": -3.972191969841845, + "open_positions": 0 }, - "positions": [ - { - "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 - } - ], + "positions": [], "signals": [ - { - "ticker": "GBPUSD=X", - "time": "2025-08-15 10:17:00+00:00", - "price": 1.3561896085739136, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, { "ticker": "EURUSD=X", - "time": "2025-08-15 10:17:00+00:00", - "price": 1.1691803932189941, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "CHFJPY=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 182.3179931640625, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "DOGE-USD", - "time": "2025-08-15 10:15:00+00:00", - "price": 0.2317201942205429, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "OP-USD", - "time": "2025-08-15 10:11:00+00:00", - "price": 0.7549265623092651, - "signal": -1, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "BTC-USD", - "time": "2025-08-15 10:15:00+00:00", - "price": 118967.421875, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "NZDUSD=X", - "time": "2025-08-15 10:16:00+00:00", - "price": 0.5927330851554871, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "EURAUD=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 1.7958300113677979, + "time": "2025-08-15 10:30:00+00:00", + "price": 1.1689070463180542, "signal": 0, "period": "7d", "interval": "1m", @@ -112,215 +24,8 @@ }, { "ticker": "USDCHF=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 0.8054800033569336, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "SOL-USD", - "time": "2025-08-15 10:15:00+00:00", - "price": 195.40365600585938, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "USDJPY=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 146.86399841308594, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "GBPJPY=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 199.14500427246094, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "XLM-USD", - "time": "2025-08-15 10:16:00+00:00", - "price": 0.42842021584510803, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "EURGBP=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 0.8619400262832642, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "LTC-USD", - "time": "2025-08-15 10:14:00+00:00", - "price": 121.15275573730469, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "USDCAD=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 1.3791099786758423, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "BNB-USD", - "time": "2025-08-15 10:15:00+00:00", - "price": 844.4680786132812, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "ETH-USD", - "time": "2025-08-15 10:16:00+00:00", - "price": 4635.28955078125, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "ETC-USD", - "time": "2025-08-15 10:15:00+00:00", - "price": 22.40416717529297, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "CADJPY=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 106.49600219726562, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "TRX-USD", - "time": "2025-08-15 10:16:00+00:00", - "price": 0.3588825762271881, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "EURJPY=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 171.65899658203125, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "TON-USD", - "time": "2025-08-15 10:11:00+00:00", - "price": 0.01708882860839367, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "ADA-USD", - "time": "2025-08-15 10:16:00+00:00", - "price": 0.951282799243927, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "NZDJPY=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 87.01699829101562, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "AUDJPY=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 95.58399963378906, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "EURCAD=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 1.611799955368042, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "BCH-USD", - "time": "2025-08-15 10:15:00+00:00", - "price": 596.3115844726562, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "DOT-USD", - "time": "2025-08-15 10:15:00+00:00", - "price": 4.005911827087402, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "ATOM-USD", - "time": "2025-08-15 10:16:00+00:00", - "price": 4.517627239227295, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "AUDNZD=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 1.0983599424362183, - "signal": 0, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "GC=F", - "time": "2025-08-15 10:08:00+00:00", - "price": 3386.10009765625, + "time": "2025-08-15 10:31:00+00:00", + "price": 0.8058599829673767, "signal": 0, "period": "7d", "interval": "1m", @@ -328,44 +33,35 @@ }, { "ticker": "NEAR-USD", - "time": "2025-08-15 10:11:00+00:00", - "price": 2.7867467403411865, - "signal": -1, - "period": "7d", - "interval": "1m", - "error": null - }, - { - "ticker": "GBPCAD=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 1.8698300123214722, + "time": "2025-08-15 10:21:00+00:00", + "price": 2.7824549674987793, "signal": 0, "period": "7d", "interval": "1m", "error": null }, { - "ticker": "XRP-USD", - "time": "2025-08-15 10:16:00+00:00", - "price": 3.111118793487549, + "ticker": "USDJPY=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 146.8820037841797, "signal": 0, "period": "7d", "interval": "1m", "error": null }, { - "ticker": "EURCHF=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 0.9409899711608887, + "ticker": "ETH-USD", + "time": "2025-08-15 10:28:00+00:00", + "price": 4645.15771484375, "signal": 0, "period": "7d", "interval": "1m", "error": null }, { - "ticker": "LINK-USD", - "time": "2025-08-15 10:16:00+00:00", - "price": 22.371389389038086, + "ticker": "CADJPY=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 106.47799682617188, "signal": 0, "period": "7d", "interval": "1m", @@ -373,8 +69,287 @@ }, { "ticker": "AUDUSD=X", - "time": "2025-08-15 10:18:00+00:00", - "price": 0.6509992480278015, + "time": "2025-08-15 10:31:00+00:00", + "price": 0.6507663130760193, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "OP-USD", + "time": "2025-08-15 10:26:00+00:00", + "price": 0.7553843259811401, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "EURJPY=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 171.62600708007812, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "NZDUSD=X", + "time": "2025-08-15 10:30:00+00:00", + "price": 0.5924872159957886, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "ETC-USD", + "time": "2025-08-15 10:30:00+00:00", + "price": 22.407447814941406, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "GC=F", + "time": "2025-08-15 10:21:00+00:00", + "price": 3386.89990234375, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "GBPJPY=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 199.0590057373047, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "AUDJPY=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 95.55599975585938, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "TRX-USD", + "time": "2025-08-15 10:29:00+00:00", + "price": 0.35983800888061523, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "TON-USD", + "time": "2025-08-15 10:26:00+00:00", + "price": 0.01708882860839367, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "LTC-USD", + "time": "2025-08-15 10:29:00+00:00", + "price": 121.18510437011719, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "EURCAD=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 1.6117500066757202, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "AUDNZD=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 1.0984100103378296, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "USDCAD=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 1.3794000148773193, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "ATOM-USD", + "time": "2025-08-15 10:29:00+00:00", + "price": 4.5298662185668945, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "BCH-USD", + "time": "2025-08-15 10:30:00+00:00", + "price": 595.4796142578125, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "LINK-USD", + "time": "2025-08-15 10:29:00+00:00", + "price": 22.34357452392578, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "XRP-USD", + "time": "2025-08-15 10:29:00+00:00", + "price": 3.117891550064087, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "DOT-USD", + "time": "2025-08-15 10:30:00+00:00", + "price": 4.020590305328369, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "BTC-USD", + "time": "2025-08-15 10:29:00+00:00", + "price": 119062.5703125, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "GBPUSD=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 1.3552889823913574, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "CHFJPY=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 182.2550048828125, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "ADA-USD", + "time": "2025-08-15 10:30:00+00:00", + "price": 0.9589924216270447, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "XLM-USD", + "time": "2025-08-15 10:29:00+00:00", + "price": 0.42904016375541687, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "EURCHF=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 0.9415299892425537, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "BNB-USD", + "time": "2025-08-15 10:30:00+00:00", + "price": 845.3945922851562, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "DOGE-USD", + "time": "2025-08-15 10:30:00+00:00", + "price": 0.23236910998821259, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "NZDJPY=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 86.98799896240234, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "EURGBP=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 0.8621399998664856, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "SOL-USD", + "time": "2025-08-15 10:29:00+00:00", + "price": 195.72109985351562, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "EURAUD=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 1.7960000038146973, + "signal": 0, + "period": "7d", + "interval": "1m", + "error": null + }, + { + "ticker": "GBPCAD=X", + "time": "2025-08-15 10:31:00+00:00", + "price": 1.869420051574707, "signal": 0, "period": "7d", "interval": "1m", diff --git a/static/app.js b/static/app.js index b25a872..26fc734 100644 --- a/static/app.js +++ b/static/app.js @@ -1,253 +1,221 @@ -// app.js +// 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) + "%"); -// ── 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 + -
- Loop: - - - - +
+

Trading Dashboard

+
+ Ostatnia aktualizacja: — + +
+
- | Auto-refresh: - - - +
+ +
+
+
Gotówka
+
+
+
+
Wartość konta (total)
+
+
+
+
Zysk (otwarte pozycje)
+
+
+
+
Otwarte pozycje
+
+
+
- | Runda: - | Gotówka: -
+ +
+

Wartość konta (total) — wykres

+
+ +
+
-
- Teraz: - - - 0 / 0 - - Ostatnia akcja: - -
+ +
+

Gotówka — wykres

+
+ +
+
-
-
-

Pozycje

- + +
+

Otwarte pozycje

+
- + + + + + + + + +
TickerStronaIlośćWejścieOstatniaPnL
TickerQtyEntrySideLast priceUnreal. PnLUnreal. PnL %
-
-
-

Transakcje (ostatnie 50)

- + + + +
+

Ostatnie sygnały

+
- + + + + + + +
CzasTickerAkcjaCenaIlość
TickerTimePriceSignalInterval
-
-
+ - + +
+

Transakcje (ostatnie 50)

+ + + + + + + + + + + + + + +
TimeActionTickerPriceQtyPnLPnL %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