This commit is contained in:
Patryk Zamorski 2025-08-15 12:19:07 +02:00
commit 861faf4ee4
38 changed files with 3652 additions and 0 deletions

3
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -0,0 +1,19 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="6">
<item index="0" class="java.lang.String" itemvalue="flask" />
<item index="1" class="java.lang.String" itemvalue="Flask" />
<item index="2" class="java.lang.String" itemvalue="tqdm" />
<item index="3" class="java.lang.String" itemvalue="Flask-HTTPAuth" />
<item index="4" class="java.lang.String" itemvalue="Flask-SocketIO" />
<item index="5" class="java.lang.String" itemvalue="requests" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

10
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.13 (pythonProject)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (pythonProject)" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/pythonProject.iml" filepath="$PROJECT_DIR$/.idea/pythonProject.iml" />
</modules>
</component>
</project>

10
.idea/pythonProject.iml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

226
README.md Normal file
View File

@ -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/<TICKER>/<TICKER>_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.

76
app.py Normal file
View File

@ -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)

57
config.py Normal file
View File

@ -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()

84
data.py Normal file
View File

@ -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

76
indicators.py Normal file
View File

@ -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

14
install.sh Normal file
View File

@ -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 &

37
io_utils.py Normal file
View File

@ -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)

26
metrics.py Normal file
View File

@ -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}

61
new/app.py Normal file
View File

@ -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)

125
new/indicators.py Normal file
View File

@ -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

BIN
new/new.zip Normal file

Binary file not shown.

333
new/portfolio.py Normal file
View File

@ -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()])

View File

@ -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
}
]

26
new/positions.json Normal file
View File

@ -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
}
]

344
new/signals_scan.json Normal file
View File

@ -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
}
]

385
new/snapshot.json Normal file
View File

@ -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
}

221
new/static/app.js Normal file
View File

@ -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 = `
<td>${p.ticker}</td>
<td>${fmt6(qty)}</td>
<td>${fmt6(entry)}</td>
<td>${side === 1 ? "LONG" : "SHORT"}</td>
<td>${fmt6(price)}</td>
<td class="${pnlClass}">${fmt2(upnl)}</td>
<td class="${pnlClass}">${fmtPct(upct)}</td>
`;
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 = `
<td>${s.ticker}</td>
<td>${s.time}</td>
<td>${fmt6(s.price)}</td>
<td class="${sigTxt}">${sigTxt}</td>
<td>${s.interval}</td>
`;
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 = `
<td>${t.time}</td>
<td class="${t.action}">${t.action}</td>
<td>${t.ticker}</td>
<td>${fmt6(t.price)}</td>
<td>${fmt6(t.qty)}</td>
<td class="${pnlClass}">${fmt2(t.pnl_abs)}</td>
<td class="${pnlClass}">${fmtPct(t.pnl_pct)}</td>
<td>${fmt2(t.cash_after)}</td>
`;
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);

28
new/static/styles.css Normal file
View File

@ -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; }

137
new/strategies.py Normal file
View File

@ -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:0021: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)

114
new/templates/index.html Normal file
View File

@ -0,0 +1,114 @@
<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Trading Dashboard</title>
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
<header>
<h1>Trading Dashboard</h1>
<div class="meta">
<span id="last-update">Ostatnia aktualizacja: —</span>
<button id="refresh-btn">Odśwież teraz</button>
</div>
</header>
<main>
<!-- KARTY -->
<section class="cards">
<div class="card">
<div class="card-title">Gotówka</div>
<div class="card-value" id="cash"></div>
</div>
<div class="card">
<div class="card-title">Wartość konta (total)</div>
<div class="card-value" id="total-value"></div>
</div>
<div class="card">
<div class="card-title">Zysk (otwarte pozycje)</div>
<div class="card-value" id="unrealized"></div>
</div>
<div class="card">
<div class="card-title">Otwarte pozycje</div>
<div class="card-value" id="open-pos"></div>
</div>
</section>
<!-- WYKRES WARTOŚCI KONTA (TOTAL) -->
<section>
<h2>Wartość konta (total) — wykres</h2>
<div class="chart-wrap">
<canvas id="totalChart" width="1200" height="300"></canvas>
</div>
</section>
<!-- WYKRES GOTÓWKI -->
<section>
<h2>Gotówka — wykres</h2>
<div class="chart-wrap">
<canvas id="cashChart" width="1200" height="300"></canvas>
</div>
</section>
<!-- Otwarte pozycje -->
<section>
<h2>Otwarte pozycje</h2>
<table id="positions-table">
<thead>
<tr>
<th>Ticker</th>
<th>Qty</th>
<th>Entry</th>
<th>Side</th>
<th>Last price</th>
<th>Unreal. PnL</th>
<th>Unreal. PnL %</th>
</tr>
</thead>
<tbody></tbody>
</table>
</section>
<!-- Ostatnie sygnały -->
<section>
<h2>Ostatnie sygnały</h2>
<table id="signals-table">
<thead>
<tr>
<th>Ticker</th>
<th>Time</th>
<th>Price</th>
<th>Signal</th>
<th>Interval</th>
</tr>
</thead>
<tbody></tbody>
</table>
</section>
<!-- Transakcje -->
<section>
<h2>Transakcje (ostatnie 50)</h2>
<table id="trades-table">
<thead>
<tr>
<th>Time</th>
<th>Action</th>
<th>Ticker</th>
<th>Price</th>
<th>Qty</th>
<th>PnL</th>
<th>PnL %</th>
<th>Cash po</th>
</tr>
</thead>
<tbody></tbody>
</table>
</section>
</main>
<script src="{{ url_for('static', filename='app.js') }}"></script>
</body>
</html>

28
new/trade_log.json Normal file
View File

@ -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": ""
}
]

157
new/trading_bot.py Normal file
View File

@ -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()

137
portfolio.py Normal file
View File

@ -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()])

BIN
pythonProject.zip Normal file

Binary file not shown.

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
yfinance
pandas
numpy
Flask

83
server.py Normal file
View File

@ -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)

253
static/app.js Normal file
View File

@ -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 = `
<td>${p.ticker}</td>
<td>${p.side}</td>
<td class="num">${fmtNum(p.size, 0)}</td>
<td class="num">${fmtNum(p.entry_price)}</td>
<td class="num">${fmtNum(p.last_price)}</td>
<td class="num">${fmtNum(p.pnl)}</td>
`;
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 = `
<td>${t.time ?? ""}</td>
<td>${t.ticker ?? ""}</td>
<td>${t.action ?? ""}</td>
<td class="num">${fmtNum(t.price)}</td>
<td class="num">${fmtNum(t.size, 0)}</td>
`;
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.");
}
});

41
static/style.css Normal file
View File

@ -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; }

93
strategy.py Normal file
View File

@ -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)

81
templates/index.html Normal file
View File

@ -0,0 +1,81 @@
<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Trader Panel</title>
<link rel="stylesheet" href="/static/style.css"/>
<style>
body { font-family: system-ui, Arial, sans-serif; margin: 0; padding: 1rem; background: #fafafa; }
.toolbar { display:flex; gap:.5rem; flex-wrap:wrap; align-items:center; margin-bottom:1rem; }
.badge { padding:.2rem .5rem; border-radius:.5rem; background:#ddd; font-weight:600; }
.badge.ok { background:#d1fae5; }
.badge.err { background:#fee2e2; }
.grid { display:grid; grid-template-columns: 1fr; gap: 1rem; }
@media(min-width: 900px){ .grid{ grid-template-columns: 1fr 1fr; } }
.card { border:1px solid #eee; border-radius:.75rem; padding:1rem; background:#fff; box-shadow:0 1px 4px rgba(0,0,0,.04); }
table { width:100%; border-collapse:collapse; }
th, td { border-bottom:1px solid #eee; padding:.4rem .5rem; }
td.num { text-align:right; font-variant-numeric: tabular-nums; }
button { cursor:pointer; }
.muted{ color:#666; }
.now { display:flex; gap:.5rem; align-items:center; }
progress { width: 220px; height: 10px; }
</style>
</head>
<body>
<div class="toolbar card">
<span>Loop:</span>
<span id="loopState" class="badge"></span>
<button id="btnStart">Start</button>
<button id="btnStop">Stop</button>
<button id="btnTick" title="Wykonaj jedną rundę (1 ticker)">Tick</button>
<span>| Auto-refresh:</span>
<button id="autoOn">On</button>
<button id="autoOff">Off</button>
<label class="muted">co <select id="refreshMs">
<option value="1000">1s</option>
<option value="2000" selected>2s</option>
<option value="5000">5s</option>
<option value="10000">10s</option>
</select></label>
<span>| Runda: <b id="roundNo"></b></span>
<span>| Gotówka: <b id="cash"></b></span>
</div>
<div class="card now">
<b>Teraz:</b>
<span id="stage"></span>
<span id="ticker"></span>
<span id="progressText" class="muted">0 / 0</span>
<progress id="progress" value="0" max="0"></progress>
<span class="muted">Ostatnia akcja:</span>
<span id="lastAction"></span>
</div>
<div class="grid">
<div class="card">
<h3>Pozycje</h3>
<table id="positions">
<thead>
<tr><th>Ticker</th><th>Strona</th><th>Ilość</th><th>Wejście</th><th>Ostatnia</th><th>PnL</th></tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="card">
<h3>Transakcje (ostatnie 50)</h3>
<table id="trades">
<thead>
<tr><th>Czas</th><th>Ticker</th><th>Akcja</th><th>Cena</th><th>Ilość</th></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<script src="/static/app.js"></script>
</body>
</html>

264
trader.py Normal file
View File

@ -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