commit 861faf4ee400d069561ccb04c9f6e69e25f2b27b
Author: Patryk Zamorski
Date: Fri Aug 15 12:19:07 2025 +0200
init
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..a63cecf
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..8ed7134
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..e15ec35
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/pythonProject.iml b/.idea/pythonProject.iml
new file mode 100644
index 0000000..2c80e12
--- /dev/null
+++ b/.idea/pythonProject.iml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d0780db
--- /dev/null
+++ b/README.md
@@ -0,0 +1,226 @@
+# 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
new file mode 100644
index 0000000..89c8574
--- /dev/null
+++ b/app.py
@@ -0,0 +1,76 @@
+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
+
+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)
+
+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)
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..c13491f
--- /dev/null
+++ b/config.py
@@ -0,0 +1,57 @@
+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
new file mode 100644
index 0000000..4ba3ff1
--- /dev/null
+++ b/data.py
@@ -0,0 +1,84 @@
+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
new file mode 100644
index 0000000..0cc2e1c
--- /dev/null
+++ b/indicators.py
@@ -0,0 +1,76 @@
+from __future__ import annotations
+import math
+from typing import Tuple
+import numpy as np
+import pandas as pd
+
+def sma(arr: np.ndarray, period: int) -> np.ndarray:
+ if len(arr) < period: return np.full(len(arr), 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)
+ 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)
+ 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]
+ 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):
+ if len(C) < period: return np.full(len(C), np.nan), np.full(len(C), 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):
+ s = pd.Series(arr)
+ ma = s.rollizng(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
+ 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):
+ 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)
+ 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
diff --git a/install.sh b/install.sh
new file mode 100644
index 0000000..b9b85b4
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,14 @@
+#!/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
new file mode 100644
index 0000000..3f7a457
--- /dev/null
+++ b/io_utils.py
@@ -0,0 +1,37 @@
+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
new file mode 100644
index 0000000..7b55542
--- /dev/null
+++ b/metrics.py
@@ -0,0 +1,26 @@
+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
new file mode 100644
index 0000000..b855b43
--- /dev/null
+++ b/new/app.py
@@ -0,0 +1,61 @@
+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
new file mode 100644
index 0000000..6ebd7e4
--- /dev/null
+++ b/new/indicators.py
@@ -0,0 +1,125 @@
+# 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
new file mode 100644
index 0000000..125111e
Binary files /dev/null and b/new/new.zip differ
diff --git a/new/portfolio.py b/new/portfolio.py
new file mode 100644
index 0000000..32b711e
--- /dev/null
+++ b/new/portfolio.py
@@ -0,0 +1,333 @@
+# 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
new file mode 100644
index 0000000..57abe6e
--- /dev/null
+++ b/new/portfolio_history.json
@@ -0,0 +1,79 @@
+[
+ {
+ "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
new file mode 100644
index 0000000..01abac4
--- /dev/null
+++ b/new/positions.json
@@ -0,0 +1,26 @@
+[
+ {
+ "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/signals_scan.json b/new/signals_scan.json
new file mode 100644
index 0000000..23a5472
--- /dev/null
+++ b/new/signals_scan.json
@@ -0,0 +1,344 @@
+[
+ {
+ "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,
+ "signal": 0,
+ "period": "7d",
+ "interval": "1m",
+ "error": null
+ },
+ {
+ "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,
+ "signal": 0,
+ "period": "7d",
+ "interval": "1m",
+ "error": null
+ },
+ {
+ "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,
+ "signal": 0,
+ "period": "7d",
+ "interval": "1m",
+ "error": null
+ },
+ {
+ "ticker": "XRP-USD",
+ "time": "2025-08-15 10:16:00+00:00",
+ "price": 3.111118793487549,
+ "signal": 0,
+ "period": "7d",
+ "interval": "1m",
+ "error": null
+ },
+ {
+ "ticker": "EURCHF=X",
+ "time": "2025-08-15 10:18:00+00:00",
+ "price": 0.9409899711608887,
+ "signal": 0,
+ "period": "7d",
+ "interval": "1m",
+ "error": null
+ },
+ {
+ "ticker": "LINK-USD",
+ "time": "2025-08-15 10:16:00+00:00",
+ "price": 22.371389389038086,
+ "signal": 0,
+ "period": "7d",
+ "interval": "1m",
+ "error": null
+ },
+ {
+ "ticker": "AUDUSD=X",
+ "time": "2025-08-15 10:18:00+00:00",
+ "price": 0.6509992480278015,
+ "signal": 0,
+ "period": "7d",
+ "interval": "1m",
+ "error": null
+ }
+]
\ No newline at end of file
diff --git a/new/snapshot.json b/new/snapshot.json
new file mode 100644
index 0000000..f97cac6
--- /dev/null
+++ b/new/snapshot.json
@@ -0,0 +1,385 @@
+{
+ "time": "2025-08-15 12:19:02",
+ "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,
+ "unrealized_pnl": 0.0,
+ "realized_pnl_cum": 0.0,
+ "open_positions": 2
+ },
+ "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
+ }
+ ],
+ "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,
+ "signal": 0,
+ "period": "7d",
+ "interval": "1m",
+ "error": null
+ },
+ {
+ "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,
+ "signal": 0,
+ "period": "7d",
+ "interval": "1m",
+ "error": null
+ },
+ {
+ "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,
+ "signal": 0,
+ "period": "7d",
+ "interval": "1m",
+ "error": null
+ },
+ {
+ "ticker": "XRP-USD",
+ "time": "2025-08-15 10:16:00+00:00",
+ "price": 3.111118793487549,
+ "signal": 0,
+ "period": "7d",
+ "interval": "1m",
+ "error": null
+ },
+ {
+ "ticker": "EURCHF=X",
+ "time": "2025-08-15 10:18:00+00:00",
+ "price": 0.9409899711608887,
+ "signal": 0,
+ "period": "7d",
+ "interval": "1m",
+ "error": null
+ },
+ {
+ "ticker": "LINK-USD",
+ "time": "2025-08-15 10:16:00+00:00",
+ "price": 22.371389389038086,
+ "signal": 0,
+ "period": "7d",
+ "interval": "1m",
+ "error": null
+ },
+ {
+ "ticker": "AUDUSD=X",
+ "time": "2025-08-15 10:18:00+00:00",
+ "price": 0.6509992480278015,
+ "signal": 0,
+ "period": "7d",
+ "interval": "1m",
+ "error": null
+ }
+ ],
+ "capital_start": 10000.0
+}
\ No newline at end of file
diff --git a/new/static/app.js b/new/static/app.js
new file mode 100644
index 0000000..26fc734
--- /dev/null
+++ b/new/static/app.js
@@ -0,0 +1,221 @@
+// 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/static/styles.css b/new/static/styles.css
new file mode 100644
index 0000000..a951116
--- /dev/null
+++ b/new/static/styles.css
@@ -0,0 +1,28 @@
+/* static/styles.css */
+:root { --bg:#0f1115; --panel:#171a21; --text:#e7e9ee; --muted:#9aa3b2; --buy:#1fbf75; --sell:#ff4d4f; }
+* { box-sizing: border-box; }
+body { margin:0; background:var(--bg); color:var(--text); font:14px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial; }
+header { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; background:var(--panel); border-bottom:1px solid #242a36; }
+h1 { margin:0; font-size:18px; }
+.meta { display:flex; gap:12px; align-items:center; color:var(--muted); }
+button { background:#2a3242; color:var(--text); border:1px solid #2f3749; padding:6px 10px; border-radius:6px; cursor:pointer; }
+main { padding:16px 20px; }
+.cards { display:grid; grid-template-columns:repeat(4, minmax(160px, 1fr)); gap:12px; margin-bottom:14px; }
+.card { background:var(--panel); border:1px solid #242a36; border-radius:10px; padding:12px; }
+.card-title { color:var(--muted); font-size:12px; text-transform:uppercase; letter-spacing:.06em; margin-bottom:6px; }
+.card-value { font-size:20px; font-weight:600; }
+section { margin-top:18px; }
+table { width:100%; border-collapse:separate; border-spacing:0; background:var(--panel); border:1px solid #242a36; border-radius:10px; overflow:hidden; }
+thead th { text-align:left; color:var(--muted); font-weight:600; padding:10px 12px; background:#141821; }
+tbody td { padding:9px 12px; border-top:1px solid #202637; }
+td.BUY, .BUY { color:var(--buy); font-weight:600; }
+td.SELL, .SELL { color:var(--sell); font-weight:600; }
+td.HOLD, .HOLD { color:#c8cbd2; }
+/* PnL kolory */
+.pnl-positive { color: var(--buy); font-weight: 600; }
+.pnl-negative { color: var(--sell); font-weight: 600; }
+.BUY { color: var(--buy); font-weight: 600; }
+.SELL { color: var(--sell); font-weight: 600; }
+.HOLD { color: #c8cbd2; }
+.chart-wrap { background: var(--panel); border:1px solid #242a36; border-radius:10px; padding:10px; margin-bottom:16px; }
+
diff --git a/new/strategies.py b/new/strategies.py
new file mode 100644
index 0000000..37cc4ae
--- /dev/null
+++ b/new/strategies.py
@@ -0,0 +1,137 @@
+# strategies.py
+from __future__ import annotations
+import numpy as np
+import pandas as pd
+
+# ===== PRESET =====
+BUY_TH = 0.55
+SELL_TH = -0.55
+REQUIRE_TREND_CONFLUENCE = True # EMA50 vs EMA200 musi się zgadzać z kierunkiem
+USE_ADX_IN_FILTER = True # używaj ADX w filtrze kierunku
+TREND_FILTER_ADX_MIN = 18.0
+MTF_CONFLICT_DAMP = 0.6 # konflikt 1m vs 15m – osłab wynik
+
+# ===== FILTRY DODATKOWE =====
+SESSION_FILTER = True
+SESS_UTC_START = 6 # handluj tylko 06:00–21:00 UTC (LON+NY)
+SESS_UTC_END = 21
+
+VOL_FILTER = True
+MIN_ATR_PCT = 0.0005 # min ATR/price (0.05%)
+MAX_ATR_PCT = 0.02 # max ATR/price (2%) – unikaj paniki
+OVEREXT_COEF = 1.2 # nie wchodź, jeśli |close-EMA20| > 1.2*ATR
+
+def _resample_ohlc(df: pd.DataFrame, rule: str = "15T") -> pd.DataFrame:
+ o = {"open":"first","high":"max","low":"min","close":"last"}
+ return df[["open","high","low","close"]].resample(rule).agg(o).dropna(how="any")
+
+def _safe(df: pd.DataFrame, col: str, default=None):
+ return float(df[col].iloc[-1]) if col in df.columns else (float(default) if default is not None else None)
+
+def _ema(series: pd.Series, span: int) -> float:
+ return float(series.ewm(span=span, adjust=False).mean().iloc[-1])
+
+def _atr_pct(df: pd.DataFrame, n: int = 14) -> float:
+ if "atr" in df.columns:
+ atr = float(df["atr"].iloc[-1])
+ else:
+ h, l, c = df["high"], df["low"], df["close"]
+ prev_c = c.shift(1)
+ tr = pd.concat([h - l, (h - prev_c).abs(), (l - prev_c).abs()], axis=1).max(axis=1)
+ atr = float(tr.ewm(span=n, adjust=False).mean().iloc[-1])
+ price = float(df["close"].iloc[-1])
+ return 0.0 if price <= 0 else atr / price
+
+def _in_session(df: pd.DataFrame) -> bool:
+ ts = pd.Timestamp(df.index[-1])
+ try:
+ hour = ts.tz_convert("UTC").hour if ts.tzinfo else ts.hour
+ except Exception:
+ hour = ts.hour
+ return SESS_UTC_START <= hour <= SESS_UTC_END
+
+def _trend_score_1m(df: pd.DataFrame) -> float:
+ s = 0.0
+ ema20 = _safe(df, "ema20", _ema(df["close"], 20))
+ ema50 = _safe(df, "ema50", _ema(df["close"], 50))
+ s += 0.5 if ema20 > ema50 else -0.5
+
+ macd = _safe(df, "macd", _ema(df["close"],12) - _ema(df["close"],26))
+ macd_sig = _safe(df, "macd_sig", macd)
+ s += 0.3 if macd > macd_sig else -0.3
+
+ adx = _safe(df, "adx", None)
+ if adx is not None:
+ if adx >= 18: s += 0.2
+ elif adx <= 12: s -= 0.1
+
+ c = float(df["close"].iloc[-1])
+ st = _safe(df, "supertrend", c)
+ s += 0.2 if c > st else -0.2
+
+ up = _safe(df, "bb_up", c + 1e9); dn = _safe(df, "bb_dn", c - 1e9)
+ if c > up: s += 0.2
+ if c < dn: s -= 0.2
+
+ rsi = _safe(df, "rsi14", 50.0)
+ if rsi >= 70: s -= 0.2
+ if rsi <= 30: s += 0.2
+ return s
+
+def _trend_score_15m(df_1m: pd.DataFrame) -> float:
+ try:
+ htf = _resample_ohlc(df_1m, "15T")
+ if len(htf) < 30: return 0.0
+ close = htf["close"]
+ ema20 = close.ewm(span=20, adjust=False).mean().iloc[-1]
+ ema50 = close.ewm(span=50, adjust=False).mean().iloc[-1]
+ macd_line = close.ewm(span=12, adjust=False).mean().iloc[-1] - close.ewm(span=26, adjust=False).mean().iloc[-1]
+ macd_sig = pd.Series(close).ewm(span=9, adjust=False).mean().iloc[-1]
+ s = (0.6 if ema20 > ema50 else -0.6) + (0.4 if macd_line > macd_sig else -0.4)
+ return float(s)
+ except Exception:
+ return 0.0
+
+def _hysteresis_map(score: float) -> int:
+ if score >= BUY_TH: return 1
+ if score <= SELL_TH: return -1
+ return 0
+
+def get_signal(df: pd.DataFrame) -> int:
+ if len(df) < 200:
+ return 0
+
+ # Filtry ex-ante
+ if SESSION_FILTER and not _in_session(df):
+ return 0
+ atrp = _atr_pct(df)
+ if VOL_FILTER and not (MIN_ATR_PCT <= atrp <= MAX_ATR_PCT):
+ return 0
+
+ s1 = _trend_score_1m(df)
+ s15 = _trend_score_15m(df)
+ score = 0.7 * s1 + 0.3 * s15
+ if (s1 > 0 and s15 < 0) or (s1 < 0 and s15 > 0):
+ score *= MTF_CONFLICT_DAMP
+ sig = _hysteresis_map(score)
+
+ # Nie handluj pod prąd + nie gonić świecy
+ if sig != 0 and REQUIRE_TREND_CONFLUENCE:
+ ema50 = _safe(df, "ema50", _ema(df["close"], 50))
+ ema200 = _safe(df, "ema200", _ema(df["close"], 200))
+ adx = _safe(df, "adx", None)
+ long_ok = (ema50 > ema200) and (not USE_ADX_IN_FILTER or adx is None or adx >= TREND_FILTER_ADX_MIN)
+ short_ok = (ema50 < ema200) and (not USE_ADX_IN_FILTER or adx is None or adx >= TREND_FILTER_ADX_MIN)
+ if sig == 1 and not long_ok: sig = 0
+ if sig == -1 and not short_ok: sig = 0
+
+ # za daleko od EMA20 -> poczekaj na pullback
+ if sig != 0:
+ ema20 = _safe(df, "ema20", _ema(df["close"], 20))
+ c = float(df["close"].iloc[-1])
+ # przy braku atr w df liczymy atrp już powyżej
+ atr = atrp * c if c > 0 else 0.0
+ if atr > 0.0 and abs(c - ema20) > OVEREXT_COEF * atr:
+ sig = 0
+
+ return int(sig)
diff --git a/new/templates/index.html b/new/templates/index.html
new file mode 100644
index 0000000..da2a5a9
--- /dev/null
+++ b/new/templates/index.html
@@ -0,0 +1,114 @@
+
+
+
+
+
+ Trading Dashboard
+
+
+
+
+
+
+
+
+
+
+
Wartość konta (total)
+
—
+
+
+
Zysk (otwarte pozycje)
+
—
+
+
+
+
+
+
+ Wartość konta (total) — wykres
+
+
+
+
+
+
+
+ Gotówka — wykres
+
+
+
+
+
+
+
+ Otwarte pozycje
+
+
+
+ | Ticker |
+ Qty |
+ Entry |
+ Side |
+ Last price |
+ Unreal. PnL |
+ Unreal. PnL % |
+
+
+
+
+
+
+
+
+ Ostatnie sygnały
+
+
+
+ | Ticker |
+ Time |
+ Price |
+ Signal |
+ Interval |
+
+
+
+
+
+
+
+
+ Transakcje (ostatnie 50)
+
+
+
+ | Time |
+ Action |
+ Ticker |
+ Price |
+ Qty |
+ PnL |
+ PnL % |
+ Cash po |
+
+
+
+
+
+
+
+
+
+
diff --git a/new/trade_log.json b/new/trade_log.json
new file mode 100644
index 0000000..2a1d04a
--- /dev/null
+++ b/new/trade_log.json
@@ -0,0 +1,28 @@
+[
+ {
+ "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/new/trading_bot.py b/new/trading_bot.py
new file mode 100644
index 0000000..e6c52bd
--- /dev/null
+++ b/new/trading_bot.py
@@ -0,0 +1,157 @@
+# trading_bot_workers.py
+# pip install yfinance pandas numpy
+
+from __future__ import annotations
+import time, warnings, random, multiprocessing as mp, json
+from dataclasses import dataclass, asdict
+from typing import Dict, List, Tuple, Optional
+import pandas as pd
+import yfinance as yf
+
+from indicators import add_indicators
+from strategies import get_signal
+from portfolio import Portfolio
+
+warnings.filterwarnings("ignore", category=FutureWarning)
+
+# ========= KONFIG =========
+START_CAPITAL = 10_000.0
+MIN_BARS = 200
+SCAN_EVERY_S = 60
+YF_JITTER_S = (0.05, 0.25)
+PRIMARY = ("7d","1m") # preferowane: 1m/7d
+FALLBACKS = [("60d","5m"), ("60d","15m")] # fallbacki gdy 1m brak
+
+FOREX_20 = [
+ "EURUSD=X","USDJPY=X","GBPUSD=X","USDCHF=X","AUDUSD=X",
+ "USDCAD=X","NZDUSD=X","EURJPY=X","EURGBP=X","EURCHF=X",
+ "GBPJPY=X","CHFJPY=X","AUDJPY=X","AUDNZD=X","EURAUD=X",
+ "EURCAD=X","GBPCAD=X","CADJPY=X","NZDJPY=X","GC=F"
+]
+CRYPTO_20 = [
+ "BTC-USD","ETH-USD","BNB-USD","SOL-USD","XRP-USD","ADA-USD","DOGE-USD","TRX-USD","TON-USD","DOT-USD",
+ "LTC-USD","BCH-USD","ATOM-USD","LINK-USD","XLM-USD","ETC-USD","NEAR-USD","OP-USD"
+]
+ALL_TICKERS = FOREX_20 + CRYPTO_20
+
+# ========= Pobieranie z fallbackiem =========
+def yf_download_with_fallback(ticker: str) -> Tuple[pd.DataFrame, str, str]:
+ time.sleep(random.uniform(*YF_JITTER_S))
+ for period, interval in (PRIMARY, *FALLBACKS):
+ df = yf.download(ticker, period=period, interval=interval, auto_adjust=False, progress=False, threads=False)
+ if df is not None and not df.empty:
+ df = df.rename(columns=str.lower).dropna()
+ if not isinstance(df.index, pd.DatetimeIndex):
+ df.index = pd.to_datetime(df.index)
+ return df, period, interval
+ raise ValueError("No data in primary/fallback intervals")
+
+# ========= Worker: jeden instrument = jeden proces =========
+@dataclass
+class Signal:
+ ticker: str
+ time: str
+ price: float
+ signal: int
+ period: str
+ interval: str
+ error: Optional[str] = None
+
+def worker(ticker: str, out_q: mp.Queue, stop_evt: mp.Event, min_bars: int = MIN_BARS):
+ while not stop_evt.is_set():
+ try:
+ df, used_period, used_interval = yf_download_with_fallback(ticker)
+ if len(df) < min_bars:
+ raise ValueError(f"Too few bars: {len(df)}<{min_bars} at {used_interval}")
+ df = add_indicators(df, min_bars=min_bars)
+ sig = get_signal(df)
+ price = float(df["close"].iloc[-1])
+ ts = str(df.index[-1])
+ out_q.put(Signal(ticker, ts, price, sig, used_period, used_interval))
+ except Exception as e:
+ out_q.put(Signal(ticker, time.strftime("%Y-%m-%d %H:%M:%S"), float("nan"), 0, "NA", "NA", error=str(e)))
+ time.sleep(SCAN_EVERY_S)
+
+# ========= Pomocnicze: serializacja do JSON =========
+def _to_native(obj):
+ """Bezpieczny rzut na prymitywy JSON (float/int/str/bool/None)."""
+ if isinstance(obj, (float, 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]
+ if hasattr(obj, "__dataclass_fields__"): # dataclass
+ return _to_native(asdict(obj))
+ try:
+ return float(obj)
+ except Exception:
+ return str(obj)
+
+def save_json(path: str, data) -> None:
+ with open(path, "w", encoding="utf-8") as f:
+ json.dump(_to_native(data), f, ensure_ascii=False, indent=2)
+
+# ========= Master (koordynacja) =========
+def main():
+ mp.set_start_method("spawn", force=True)
+ out_q: mp.Queue = mp.Queue()
+ stop_evt: mp.Event = mp.Event()
+
+ # 1 instrument = 1 proces (daemon)
+ procs: List[mp.Process] = []
+ for t in ALL_TICKERS:
+ p = mp.Process(target=worker, args=(t, out_q, stop_evt), daemon=True)
+ p.start()
+ procs.append(p)
+
+ portfolio = Portfolio(START_CAPITAL)
+ last_dump = 0.0
+ signals_window: Dict[str, Signal] = {}
+
+ try:
+ while True:
+ # zbieramy sygnały przez okno ~1 minuty
+ deadline = time.time() + SCAN_EVERY_S
+ while time.time() < deadline:
+ try:
+ s: Signal = out_q.get(timeout=0.5)
+ signals_window[s.ticker] = s
+ except Exception:
+ pass
+
+ if signals_window:
+ sig_list = list(signals_window.values())
+ portfolio.on_signals(sig_list)
+
+ now = time.time()
+ if now - last_dump > SCAN_EVERY_S - 1:
+ # --- JSON zapisy ---
+ save_json("signals_scan.json", [asdict(s) for s in sig_list])
+ save_json("portfolio_history.json", portfolio.history)
+ save_json("positions.json", [{"ticker": t, **p} for t, p in portfolio.positions.items()])
+
+ snapshot = {
+ "time": time.strftime("%Y-%m-%d %H:%M:%S"),
+ "last_history": portfolio.history[-1] if portfolio.history else None,
+ "positions": [{"ticker": t, **p} for t, p in portfolio.positions.items()],
+ "signals": [asdict(s) for s in sig_list],
+ "capital_start": START_CAPITAL
+ }
+ save_json("snapshot.json", snapshot)
+
+ last_dump = now
+
+ if portfolio.history:
+ print(pd.DataFrame(portfolio.history).tail(1).to_string(index=False))
+ else:
+ print("Brak sygnałów w oknie – czekam...")
+
+ except KeyboardInterrupt:
+ print("\nStopping workers...")
+ stop_evt.set()
+ for p in procs:
+ p.join(timeout=5)
+
+if __name__ == "__main__":
+ main()
diff --git a/portfolio.py b/portfolio.py
new file mode 100644
index 0000000..cee0213
--- /dev/null
+++ b/portfolio.py
@@ -0,0 +1,137 @@
+# portfolio.py
+from __future__ import annotations
+import math, time, json
+from typing import Dict, List, Any
+
+def _to_native(obj: Any):
+ """Bezpieczne rzutowanie do typów akceptowalnych przez JSON."""
+ if isinstance(obj, (float, 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)
+
+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.
+ """
+ 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 # skumulowany realized PnL
+
+ def mark_to_market(self, prices: Dict[str, float]) -> float:
+ unreal = 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
+
+ def _log_trade(self, action: str, ticker: str, price: float, qty: float, side: int,
+ pnl_abs: float = 0.0, pnl_pct: float = 0.0):
+ 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
+ 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)
+ })
+
+ 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 = []
+ 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"))):
+ 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})
+
+ if not clean:
+ self.history.append({
+ "time": time.strftime("%Y-%m-%d %H:%M:%S"),
+ "equity": self.cash,
+ "cash": self.cash,
+ "open_positions": 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)
+
+ for s in clean:
+ t, sig, price = s["ticker"], int(s["signal"]), float(s["price"])
+
+ # zamknięcie lub odwrócenie
+ 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.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]
+
+ # 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)
+
+ 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)
+ })
+
+ # zapis plików JSON
+ 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/pythonProject.zip b/pythonProject.zip
new file mode 100644
index 0000000..9bf7ff5
Binary files /dev/null and b/pythonProject.zip differ
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..2ab18e8
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+yfinance
+pandas
+numpy
+Flask
\ No newline at end of file
diff --git a/server.py b/server.py
new file mode 100644
index 0000000..3a1d3b3
--- /dev/null
+++ b/server.py
@@ -0,0 +1,83 @@
+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/static/app.js b/static/app.js
new file mode 100644
index 0000000..b25a872
--- /dev/null
+++ b/static/app.js
@@ -0,0 +1,253 @@
+// app.js
+
+// ── 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) };
+}
+
+function makeUrl(path) {
+ return `${API_BASE}${path}${path.includes("?") ? "&" : "?"}_ts=${Date.now()}`;
+}
+
+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."
+ );
+ }
+}
+
+// ── 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);
+ 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();
+ }
+}
+
+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,
+ });
+ 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 || "–"}`);
+
+ const roundEl = document.getElementById("roundNo");
+ if (roundEl) roundEl.textContent = s.round ?? "–";
+
+ const cashEl = document.getElementById("cash");
+ if (cashEl) cashEl.textContent = fmtNum(s.cash, 2);
+
+ // now-playing
+ const stageEl = document.getElementById("stage");
+ if (stageEl) stageEl.textContent = (s.stage || "idle").toUpperCase();
+
+ const tickerEl = document.getElementById("ticker");
+ if (tickerEl) tickerEl.textContent = s.current_ticker || "–";
+
+ const idx = s.current_index ?? 0;
+ const total = s.tickers_total ?? 0;
+
+ 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)} |
+ `;
+ tbody.appendChild(tr);
+ }
+ } catch (e) {
+ console.error("positions error:", e);
+ }
+}
+
+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) => {
+ const tr = document.createElement("tr");
+ tr.innerHTML = `
+ ${t.time ?? ""} |
+ ${t.ticker ?? ""} |
+ ${t.action ?? ""} |
+ ${fmtNum(t.price)} |
+ ${fmtNum(t.size, 0)} |
+ `;
+ tbody.appendChild(tr);
+ });
+ } catch (e) {
+ console.error("trades error:", e);
+ }
+}
+
+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.");
+ }
+});
diff --git a/static/style.css b/static/style.css
new file mode 100644
index 0000000..17f4d82
--- /dev/null
+++ b/static/style.css
@@ -0,0 +1,41 @@
+: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/strategy.py b/strategy.py
new file mode 100644
index 0000000..bbb3c2b
--- /dev/null
+++ b/strategy.py
@@ -0,0 +1,93 @@
+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
new file mode 100644
index 0000000..c55391a
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,81 @@
+
+
+
+
+
+ Trader – Panel
+
+
+
+
+
+ Loop:
+ –
+
+
+
+
+ | Auto-refresh:
+
+
+
+
+ | Runda: –
+ | Gotówka: –
+
+
+
+
Teraz:
+
–
+
–
+
0 / 0
+
+
Ostatnia akcja:
+
–
+
+
+
+
+
Pozycje
+
+
+ | Ticker | Strona | Ilość | Wejście | Ostatnia | PnL |
+
+
+
+
+
+
Transakcje (ostatnie 50)
+
+
+ | Czas | Ticker | Akcja | Cena | Ilość |
+
+
+
+
+
+
+
+
+
diff --git a/trader.py b/trader.py
new file mode 100644
index 0000000..8c31d16
--- /dev/null
+++ b/trader.py
@@ -0,0 +1,264 @@
+# 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