init2
This commit is contained in:
parent
861faf4ee4
commit
7e722ca77f
226
README.md
226
README.md
@ -1,226 +0,0 @@
|
||||
# Multi-Asset Portfolio Simulator (Yahoo Finance, Python)
|
||||
|
||||
Szybki symulator wielo-aktywny z jedną wspólną kasą.
|
||||
Działa w pętli co 2 minuty i dla paczki tickerów:
|
||||
|
||||
- pobiera **batch** danych z Yahoo jednym zapytaniem (`yfinance.download`),
|
||||
- **dopina tylko nowe świece** (cache ostatniego czasu per ticker),
|
||||
- liczy sygnał na **close** każdej świecy,
|
||||
- **egzekwuje** zlecenie na **kolejnym open** (bardziej realistycznie),
|
||||
- prowadzi portfel (wspólne saldo, SL/TP intrabar, prowizja, poślizg),
|
||||
- zapisuje wyniki do `dane/` oraz liczy metryki **MaxDD, Sharpe, CAGR**.
|
||||
|
||||
> Uwaga: to **symulacja** oparta o dane z Yahoo. Nie składa prawdziwych zleceń.
|
||||
|
||||
---
|
||||
|
||||
## Funkcje (skrót)
|
||||
|
||||
- Multi-asset (~20 domyślnych: krypto + forex + złoto `GC=F`)
|
||||
- Batch download (znacznie szybciej niż pojedynczo)
|
||||
- Tylko nowe świece (bez pobierania pełnej historii co rundę)
|
||||
- Sygnał: EMA200 + SMA(20/50) + RSI(14) + ATR-SL/TP + filtry:
|
||||
**MACD, Stochastic, Bollinger, ADX, Supertrend**
|
||||
- Egzekucja na **kolejnym OPEN**
|
||||
- Portfel: **jedna** kasa dla wszystkich instrumentów, risk per trade (% equity)
|
||||
- Zapisy CSV + **portfolio_summary** z MaxDD / Sharpe (annual) / CAGR
|
||||
|
||||
---
|
||||
|
||||
## Wymagania
|
||||
|
||||
- Python 3.9+
|
||||
- `pip install yfinance pandas numpy`
|
||||
|
||||
---
|
||||
|
||||
## Szybki start
|
||||
|
||||
```bash
|
||||
# 1) instalacja
|
||||
chmod +x install.sh
|
||||
./install.sh
|
||||
|
||||
# 2) uruchomienie (wirtualne środowisko)
|
||||
source venv/bin/activate
|
||||
python app.py
|
||||
|
||||
# (opcjonalnie w tle)
|
||||
nohup python app.py > output.log 2>&1 &
|
||||
```
|
||||
|
||||
Logi lecą do `stdout` (lub do `output.log` przy `nohup`).
|
||||
|
||||
---
|
||||
|
||||
## Struktura plików
|
||||
|
||||
```
|
||||
project/
|
||||
├─ app.py # główna pętla: batch → nowe świece → decyzje → egzekucja → zapis
|
||||
├─ config.py # konfiguracja (tickery, interwał, risk, katalog 'dane', itp.)
|
||||
├─ data.py # fetch_batch(): pobieranie paczki tickerów z yfinance
|
||||
├─ indicators.py # implementacje wskaźników (SMA/EMA/RSI/ATR/MACD/…)
|
||||
├─ strategy.py # evaluate_signal(): logika generowania sygnałów + SL/TP + rpu
|
||||
├─ portfolio.py # model portfela: pozycje, pending orders, SL/TP, PnL, equity
|
||||
├─ metrics.py # metryki portfelowe: MaxDD, Sharpe (annualized), CAGR
|
||||
├─ io_utils.py # zapisy do 'dane/': trades, equity, summary; tworzenie katalogów
|
||||
├─ requirements.txt # zależności (yfinance, pandas, numpy)
|
||||
└─ install.sh # instalator: venv + pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Co jest w każdym pliku?
|
||||
|
||||
- **`config.py`**
|
||||
Definiuje `CFG` (tickers, interwał, okres pobierania `yf_period`, minimalna historia, risk, kasa startowa, SL/TP, katalog wyjściowy).
|
||||
Zmienisz tu listę instrumentów lub parametry ryzyka.
|
||||
|
||||
- **`data.py`**
|
||||
`fetch_batch(tickers, period, interval)` — jedno zapytanie do Yahoo dla wielu tickerów. Zwraca słownik `{ticker: DataFrame}` z kolumnami `open, high, low, close, volume`.
|
||||
|
||||
- **`indicators.py`**
|
||||
Wskaźniki: `sma, ema, rsi, atr, macd, stoch_kd, bollinger, adx_val, supertrend`.
|
||||
|
||||
- **`strategy.py`**
|
||||
`evaluate_signal(df)` → `Decision(signal, sl, tp, rpu)`
|
||||
- **signal**: `BUY` / `SELL` / `NONE`
|
||||
- **sl/tp**: poziomy na bazie ATR i RR
|
||||
- **rpu** (*risk per unit*): ile ryzyka na jednostkę (do wyliczenia wielkości pozycji)
|
||||
|
||||
- **`portfolio.py`**
|
||||
Model portfela z jedną kasą:
|
||||
- egzekucja **na kolejnym OPEN** (pending orders),
|
||||
- SL/TP intrabar,
|
||||
- prowizja i poślizg,
|
||||
- `portfolio_equity` (czas, equity) i lista `trades`.
|
||||
|
||||
- **`metrics.py`**
|
||||
Metryki portfela na bazie krzywej equity:
|
||||
- **Max Drawdown** – minimalna wartość `equity/rolling_max - 1`,
|
||||
- **Sharpe annualized** – z 1-min zwrotów (525 600 okresów/rok),
|
||||
- **CAGR** – roczna złożona stopa wzrostu.
|
||||
|
||||
- **`io_utils.py`**
|
||||
Tworzy strukturę `dane/`, zapisuje:
|
||||
- `dane/portfolio_equity.csv` – equity w czasie,
|
||||
- `dane/portfolio_summary.txt` – JSON z metrykami i statystyką,
|
||||
- `dane/<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.
|
||||
138
app.py
138
app.py
@ -1,76 +1,70 @@
|
||||
from __future__ import annotations
|
||||
import time
|
||||
from typing import Dict
|
||||
import pandas as pd
|
||||
from config import CFG
|
||||
from data import fetch_batch
|
||||
from strategy import evaluate_signal
|
||||
from portfolio import Portfolio
|
||||
from io_utils import ensure_dirs, save_outputs
|
||||
import os, json, threading
|
||||
from flask import Flask, jsonify, render_template, abort
|
||||
from trading_bot import main as trading_bot_main # dostosuj jeśli moduł nazywa się inaczej
|
||||
|
||||
def interval_to_timedelta(interval: str) -> pd.Timedelta:
|
||||
# prosta mapka dla 1m/2m/5m/15m/30m/60m/1h
|
||||
mapping = {
|
||||
"1m":"1min","2m":"2min","5m":"5min","15m":"15min","30m":"30min",
|
||||
"60m":"60min","90m":"90min","1h":"60min"
|
||||
}
|
||||
key = mapping.get(interval, "1min")
|
||||
return pd.to_timedelta(key)
|
||||
# Katalog projektu (tam gdzie leży ten plik)
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Domyślnie dane są w głównym katalogu projektu; można nadpisać przez env DATA_DIR
|
||||
DATA_DIR = os.environ.get("DATA_DIR", BASE_DIR)
|
||||
|
||||
def _load_json(name: str):
|
||||
path = os.path.join(DATA_DIR, name)
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
app = Flask(
|
||||
__name__,
|
||||
static_folder="static",
|
||||
static_url_path="/static", # <-- ważne: z wiodącym slashem
|
||||
template_folder="templates",
|
||||
)
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html")
|
||||
|
||||
@app.route("/api/snapshot")
|
||||
def api_snapshot():
|
||||
data = _load_json("snapshot.json")
|
||||
if data is None:
|
||||
abort(404, description="snapshot.json not found")
|
||||
return jsonify(data)
|
||||
|
||||
@app.route("/api/history")
|
||||
def api_history():
|
||||
data = _load_json("portfolio_history.json")
|
||||
if data is None:
|
||||
abort(404, description="portfolio_history.json not found")
|
||||
return jsonify(data)
|
||||
|
||||
@app.route("/api/positions")
|
||||
def api_positions():
|
||||
data = _load_json("positions.json")
|
||||
if data is None:
|
||||
data = []
|
||||
return jsonify(data)
|
||||
|
||||
@app.route("/api/trades")
|
||||
def api_trades():
|
||||
data = _load_json("trade_log.json")
|
||||
if data is None:
|
||||
data = []
|
||||
return jsonify(data)
|
||||
|
||||
def start_trading_bot():
|
||||
"""Uruchamia bota w osobnym wątku."""
|
||||
trading_thread = threading.Thread(target=trading_bot_main, daemon=True)
|
||||
trading_thread.start()
|
||||
print("[Flask] Trading bot uruchomiony w wątku.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
ensure_dirs(CFG.root_dir, CFG.tickers)
|
||||
portfolio = Portfolio(starting_cash=CFG.starting_cash, commission_per_trade=1.0, slippage_bp=1.0)
|
||||
|
||||
last_ts: Dict[str, pd.Timestamp] = {}
|
||||
hist: Dict[str, pd.DataFrame] = {}
|
||||
bar_delta = interval_to_timedelta(CFG.interval)
|
||||
|
||||
while True:
|
||||
round_t0 = time.time()
|
||||
batch = fetch_batch(CFG.tickers, CFG.yf_period, CFG.interval)
|
||||
|
||||
for tk, df in batch.items():
|
||||
if df.empty:
|
||||
continue
|
||||
df = df.copy()
|
||||
df.index = pd.to_datetime(df.index, utc=True)
|
||||
|
||||
prev = hist.get(tk)
|
||||
if prev is not None and not prev.empty:
|
||||
df_all = pd.concat([prev, df[~df.index.isin(prev.index)]], axis=0).sort_index()
|
||||
else:
|
||||
df_all = df.sort_index()
|
||||
hist[tk] = df_all.tail(2000)
|
||||
|
||||
last = last_ts.get(tk)
|
||||
new_part = df_all[df_all.index > last] if last is not None else df_all
|
||||
|
||||
if not new_part.empty:
|
||||
for ts, row in new_part.iterrows():
|
||||
o,h,l,c = float(row["open"]), float(row["high"]), float(row["low"]), float(row["close"])
|
||||
|
||||
# 1) egzekucja oczekujących na OPEN tego baraz
|
||||
portfolio.on_new_bar(tk, ts, o,h,l,c)
|
||||
|
||||
# 2) sygnał na CLOSE → plan na KOLEJNY OPEN
|
||||
df_upto = hist[tk].loc[:ts]
|
||||
dec = evaluate_signal(df_upto)
|
||||
portfolio.schedule_order(
|
||||
tk, dec.signal, dec.rpu, dec.sl, dec.tp,
|
||||
next_bar_ts=ts + pd.to_timedelta(1, unit="min"),
|
||||
ref_price=float(df_upto["close"].iloc[-1])
|
||||
)
|
||||
|
||||
last_ts[tk] = new_part.index[-1]
|
||||
|
||||
# zapis
|
||||
import pandas as pd
|
||||
trades_df = pd.DataFrame(portfolio.trades)
|
||||
eq_df = pd.DataFrame(portfolio.portfolio_equity, columns=["time","equity"])
|
||||
save_outputs(CFG.root_dir, CFG.tickers, trades_df, eq_df, portfolio.cash)
|
||||
|
||||
elapsed = time.time() - round_t0
|
||||
sleep_s = max(0, 120 - elapsed)
|
||||
print(f"Runda OK ({elapsed:.1f}s). Pauza {sleep_s:.1f}s.")
|
||||
print(sleep_s)
|
||||
time.sleep(sleep_s)
|
||||
print(f"[Flask] DATA_DIR: {DATA_DIR}")
|
||||
start_trading_bot()
|
||||
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)), debug=True)
|
||||
|
||||
57
config.py
57
config.py
@ -1,57 +0,0 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
# 20 FX
|
||||
fx: List[str] = field(default_factory=lambda: [
|
||||
"EURUSD=X","GBPUSD=X","USDJPY=X","USDCHF=X","USDCAD=X",
|
||||
"AUDUSD=X","NZDUSD=X","EURGBP=X","EURJPY=X","EURCHF=X",
|
||||
"GBPJPY=X","AUDJPY=X","CHFJPY=X","CADJPY=X","EURAUD=X",
|
||||
"EURNZD=X","GBPAUD=X","GBPCAD=X","AUDCAD=X","NZDJPY=X",
|
||||
])
|
||||
# 20 CRYPTO
|
||||
crypto: List[str] = field(default_factory=lambda: [
|
||||
"BTC-USD","ETH-USD","BNB-USD","SOL-USD","XRP-USD",
|
||||
"ADA-USD","DOGE-USD","TRX-USD","DOT-USD","AVAX-USD",
|
||||
"MATIC-USD","LTC-USD","BCH-USD","LINK-USD","ATOM-USD",
|
||||
"XMR-USD","XLM-USD","ETC-USD","FIL-USD","NEAR-USD",
|
||||
])
|
||||
|
||||
yf_period: str = "5d"
|
||||
interval: str = "1m"
|
||||
starting_cash: float = 10000.0
|
||||
loop_sleep_s: int = 2
|
||||
root_dir: str = "out"
|
||||
commission_per_trade: float = 1.0
|
||||
slippage_bp: float = 1.0
|
||||
|
||||
# — parametry strategii —
|
||||
history_min_bars: int = 220
|
||||
rsi_len: int = 14
|
||||
|
||||
# filtry – poluzowane
|
||||
require_trend: bool = False # ignoruj EMA200 gdy False
|
||||
use_macd_filter: bool = False # ignoruj MACD gdy False
|
||||
adx_min: float = 8.0
|
||||
atr_min_frac_price: float = 1e-4
|
||||
rsi_buy_max: float = 75.0
|
||||
rsi_sell_min: float = 25.0
|
||||
|
||||
# SL/TP
|
||||
sl_atr_mult: float = 1.3
|
||||
tp_rr: float = 1.5
|
||||
|
||||
# sizing na ryzyko
|
||||
risk_per_trade_frac: float = 0.02 # 2% equity na 1R
|
||||
min_size: float = 1.0
|
||||
max_size: float = 100000.0
|
||||
|
||||
# shorty
|
||||
allow_short: bool = True
|
||||
|
||||
@property
|
||||
def tickers(self) -> List[str]:
|
||||
return self.fx + self.crypto
|
||||
|
||||
CFG = Settings()
|
||||
84
data.py
84
data.py
@ -1,84 +0,0 @@
|
||||
from __future__ import annotations
|
||||
from typing import Dict, List
|
||||
import time, logging
|
||||
import pandas as pd
|
||||
import yfinance as yf
|
||||
|
||||
log = logging.getLogger("data")
|
||||
|
||||
def _normalize_ohlc(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Ujednolica kolumny do: Open, High, Low, Close, Volume. Obsługa lowercase, MultiIndex, Adj Close."""
|
||||
if df is None or len(df) == 0:
|
||||
return pd.DataFrame(columns=["Open","High","Low","Close","Volume"])
|
||||
|
||||
df = df.copy()
|
||||
|
||||
# Flatten MultiIndex -> zwykłe nazwy
|
||||
if isinstance(df.columns, pd.MultiIndex):
|
||||
df.columns = [str(tuple(filter(None, map(str, c)))).strip("()").replace("'", "").replace(" ", "") if isinstance(c, tuple) else str(c) for c in df.columns]
|
||||
# po flatten często nazwy są np. 'Close,EURUSD=X' – weź pierwszy człon przed przecinkiem
|
||||
df.columns = [c.split(",")[0] for c in df.columns]
|
||||
|
||||
# Zrzuć TZ
|
||||
if isinstance(df.index, pd.DatetimeIndex) and df.index.tz is not None:
|
||||
df.index = df.index.tz_localize(None)
|
||||
|
||||
# Ujednolicenie do TitleCase
|
||||
norm = {c: str(c).strip() for c in df.columns}
|
||||
# mapuj najczęstsze warianty
|
||||
mapping = {}
|
||||
for c in norm.values():
|
||||
lc = c.lower()
|
||||
if lc in ("open", "op", "o"): mapping[c] = "Open"
|
||||
elif lc in ("high", "hi", "h"): mapping[c] = "High"
|
||||
elif lc in ("low", "lo", "l"): mapping[c] = "Low"
|
||||
elif lc in ("close", "cl", "c"): mapping[c] = "Close"
|
||||
elif lc in ("adj close","adjclose","adjustedclose"): mapping[c] = "Adj Close"
|
||||
elif lc in ("volume","vol","v"): mapping[c] = "Volume"
|
||||
else:
|
||||
# zostaw jak jest (np. 'Dividends', 'Stock Splits')
|
||||
mapping[c] = c
|
||||
|
||||
df.rename(columns=mapping, inplace=True)
|
||||
|
||||
# Jeśli brak Close, ale jest Adj Close -> użyj go
|
||||
if "Close" not in df.columns and "Adj Close" in df.columns:
|
||||
df["Close"] = df["Adj Close"]
|
||||
|
||||
# Upewnij się, że są wszystkie podstawowe kolumny (dodaj puste jeśli brak)
|
||||
for need in ["Open","High","Low","Close","Volume"]:
|
||||
if need not in df.columns:
|
||||
df[need] = pd.NA
|
||||
|
||||
# Pozostaw tylko rdzeń (kolejność stała)
|
||||
df = df[["Open","High","Low","Close","Volume"]]
|
||||
return df
|
||||
|
||||
def _fetch_single(ticker: str, period: str, interval: str, tries: int = 3, sleep_s: float = 0.7) -> pd.DataFrame:
|
||||
"""Pobierz OHLC dla 1 tickera (z retry) i znormalizuj kolumny."""
|
||||
for i in range(1, tries + 1):
|
||||
try:
|
||||
log.info("Yahoo: get %s (try %d/%d) period=%s interval=%s", ticker, i, tries, period, interval)
|
||||
df = yf.Ticker(ticker).history(period=period, interval=interval, auto_adjust=False, prepost=False)
|
||||
df = _normalize_ohlc(df)
|
||||
if len(df) == 0 or "Close" not in df.columns:
|
||||
log.warning("Yahoo: %s -> EMPTY or no Close (cols=%s)", ticker, list(df.columns))
|
||||
return df
|
||||
except Exception as e:
|
||||
log.warning("Yahoo: error %s (try %d/%d): %s", ticker, i, tries, e)
|
||||
time.sleep(sleep_s)
|
||||
# po niepowodzeniu zwróć pusty rdzeń
|
||||
return pd.DataFrame(columns=["Open","High","Low","Close","Volume"])
|
||||
|
||||
def fetch_batch(tickers: List[str], period: str, interval: str) -> Dict[str, pd.DataFrame]:
|
||||
"""Pobierz paczkę danych dla listy tickerów."""
|
||||
out: Dict[str, pd.DataFrame] = {}
|
||||
for tk in tickers:
|
||||
df = _fetch_single(tk, period, interval)
|
||||
out[tk] = df
|
||||
if len(df):
|
||||
first = str(df.index[0]); last = str(df.index[-1])
|
||||
else:
|
||||
first = last = "-"
|
||||
log.info("Yahoo: %s -> bars=%d %s..%s cols=%s", tk, len(df), first, last, list(df.columns))
|
||||
return out
|
||||
@ -1,33 +1,50 @@
|
||||
# indicators.py
|
||||
from __future__ import annotations
|
||||
import math
|
||||
from typing import Tuple
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
# ===== Helper =====
|
||||
def _as_1d(a) -> np.ndarray:
|
||||
return np.asarray(a, dtype=float).ravel()
|
||||
|
||||
# ===== Indicators =====
|
||||
def sma(arr: np.ndarray, period: int) -> np.ndarray:
|
||||
if len(arr) < period: return np.full(len(arr), np.nan)
|
||||
arr = _as_1d(arr); n = len(arr)
|
||||
if n == 0: return np.array([], float)
|
||||
if n < period: return np.full(n, np.nan)
|
||||
w = np.ones(period) / period
|
||||
out = np.convolve(arr, w, mode="valid")
|
||||
return np.concatenate([np.full(period-1, np.nan), out])
|
||||
|
||||
def ema(arr: np.ndarray, period: int) -> np.ndarray:
|
||||
out = np.full(len(arr), np.nan, dtype=float); k = 2/(period+1); e = np.nan
|
||||
for i,x in enumerate(arr):
|
||||
e = x if math.isnan(e) else x*k + e*(1-k)
|
||||
arr = _as_1d(arr); n = len(arr)
|
||||
out = np.full(n, np.nan, float)
|
||||
if n == 0: return out
|
||||
k = 2/(period+1); e = np.nan
|
||||
for i, x in enumerate(arr):
|
||||
e = x if (isinstance(e, float) and math.isnan(e)) else x*k + e*(1-k)
|
||||
out[i] = e
|
||||
return out
|
||||
|
||||
def rsi(arr: np.ndarray, period: int=14) -> np.ndarray:
|
||||
if len(arr) < period+1: return np.full(len(arr), np.nan)
|
||||
d = np.diff(arr, prepend=arr[0]); g = np.where(d>0,d,0.0); L = np.where(d<0,-d,0.0)
|
||||
ag, al = ema(g,period), ema(L,period); rs = ag/(al+1e-9)
|
||||
def rsi(arr: np.ndarray, period: int = 14) -> np.ndarray:
|
||||
arr = _as_1d(arr); n = len(arr)
|
||||
if n == 0: return np.array([], float)
|
||||
if n < period+1: return np.full(n, np.nan)
|
||||
d = np.diff(arr, prepend=arr[0])
|
||||
g = np.where(d > 0, d, 0.0)
|
||||
L = np.where(d < 0, -d, 0.0)
|
||||
ag, al = ema(g, period), ema(L, period)
|
||||
rs = ag / (al + 1e-9)
|
||||
return 100 - (100 / (1 + rs))
|
||||
|
||||
def atr(H: np.ndarray, L: np.ndarray, C: np.ndarray, period: int=14) -> np.ndarray:
|
||||
if len(C) < period+1: return np.full(len(C), np.nan)
|
||||
pc = np.roll(C,1)
|
||||
tr = np.maximum.reduce([H-L, np.abs(H-pc), np.abs(L-pc)])
|
||||
tr[0] = H[0]-L[0]
|
||||
def atr(H: np.ndarray, L: np.ndarray, C: np.ndarray, period: int = 14) -> np.ndarray:
|
||||
H, L, C = _as_1d(H), _as_1d(L), _as_1d(C); n = len(C)
|
||||
if n == 0: return np.array([], float)
|
||||
if n < period+1: return np.full(n, np.nan)
|
||||
pc = np.roll(C, 1)
|
||||
tr = np.maximum.reduce([H - L, np.abs(H - pc), np.abs(L - pc)])
|
||||
tr[0] = H[0] - L[0]
|
||||
return ema(tr, period)
|
||||
|
||||
def macd(arr: np.ndarray, fast=12, slow=26, signal=9):
|
||||
@ -38,7 +55,11 @@ def macd(arr: np.ndarray, fast=12, slow=26, signal=9):
|
||||
return line, sig, hist
|
||||
|
||||
def stoch_kd(H: np.ndarray, L: np.ndarray, C: np.ndarray, period=14, smooth=3):
|
||||
if len(C) < period: return np.full(len(C), np.nan), np.full(len(C), np.nan)
|
||||
H, L, C = _as_1d(H), _as_1d(L), _as_1d(C); n = len(C)
|
||||
if n == 0:
|
||||
z = np.array([], float); return z, z
|
||||
if n < period:
|
||||
return np.full(n, np.nan), np.full(n, np.nan)
|
||||
lowest = pd.Series(L).rolling(period, min_periods=1).min().to_numpy()
|
||||
highest = pd.Series(H).rolling(period, min_periods=1).max().to_numpy()
|
||||
k = 100 * (C - lowest) / (highest - lowest + 1e-9)
|
||||
@ -46,31 +67,59 @@ def stoch_kd(H: np.ndarray, L: np.ndarray, C: np.ndarray, period=14, smooth=3):
|
||||
return k, d
|
||||
|
||||
def bollinger(arr: np.ndarray, period=20, dev=2.0):
|
||||
arr = _as_1d(arr); n = len(arr)
|
||||
if n == 0:
|
||||
z = np.array([], float); return z, z, z
|
||||
s = pd.Series(arr)
|
||||
ma = s.rollizng(period, min_periods=1).mean().to_numpy()
|
||||
ma = s.rolling(period, min_periods=1).mean().to_numpy()
|
||||
sd = s.rolling(period, min_periods=1).std(ddof=0).to_numpy()
|
||||
return ma, ma + dev*sd, ma - dev*sd
|
||||
|
||||
def adx_val(H: np.ndarray, L: np.ndarray, C: np.ndarray, period=14):
|
||||
up_move = H - np.roll(H,1); down_move = np.roll(L,1) - L
|
||||
H, L, C = _as_1d(H), _as_1d(L), _as_1d(C); n = len(C)
|
||||
if n == 0:
|
||||
z = np.array([], float); return z, z, z
|
||||
up_move = H - np.roll(H, 1); down_move = np.roll(L, 1) - L
|
||||
up_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0.0)
|
||||
down_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0.0)
|
||||
tr1 = H - L; tr2 = np.abs(H - np.roll(C,1)); tr3 = np.abs(L - np.roll(C,1))
|
||||
tr1 = H - L; tr2 = np.abs(H - np.roll(C, 1)); tr3 = np.abs(L - np.roll(C, 1))
|
||||
tr = np.maximum.reduce([tr1, tr2, tr3]); tr[0] = tr1[0]
|
||||
atr14 = ema(tr, period)
|
||||
pdi = 100 * ema(up_dm, period)/(atr14+1e-9)
|
||||
mdi = 100 * ema(down_dm, period)/(atr14+1e-9)
|
||||
dx = 100 * np.abs(pdi - mdi)/(pdi + mdi + 1e-9)
|
||||
pdi = 100 * ema(up_dm, period) / (atr14 + 1e-9)
|
||||
mdi = 100 * ema(down_dm, period) / (atr14 + 1e-9)
|
||||
dx = 100 * np.abs(pdi - mdi) / (pdi + mdi + 1e-9)
|
||||
adx = ema(dx, period)
|
||||
return adx, pdi, mdi
|
||||
|
||||
def supertrend(H: np.ndarray, L: np.ndarray, C: np.ndarray, period=10, mult=3.0):
|
||||
H, L, C = _as_1d(H), _as_1d(L), _as_1d(C); n = len(C)
|
||||
if n == 0: return np.array([], float)
|
||||
a = atr(H, L, C, period).copy()
|
||||
hl2 = (H + L)/2.0
|
||||
ub = hl2 + mult*a
|
||||
lb = hl2 - mult*a
|
||||
n = len(C); st = np.full(n, np.nan)
|
||||
hl2 = (H + L) / 2.0
|
||||
ub = hl2 + mult * a
|
||||
lb = hl2 - mult * a
|
||||
st = np.full(n, np.nan)
|
||||
for i in range(1, n):
|
||||
prev = st[i-1] if not np.isnan(st[i-1]) else hl2[i-1]
|
||||
st[i] = ub[i] if C[i-1] <= prev else lb[i]
|
||||
return st
|
||||
|
||||
# ===== Pipeline: add_indicators =====
|
||||
def add_indicators(df: pd.DataFrame, min_bars: int = 200) -> pd.DataFrame:
|
||||
if len(df) < min_bars:
|
||||
raise ValueError(f"Too few bars: {len(df)} < {min_bars}")
|
||||
C, H, L = df["close"].to_numpy(), df["high"].to_numpy(), df["low"].to_numpy()
|
||||
df["ema20"] = ema(C, 20)
|
||||
df["ema50"] = ema(C, 50)
|
||||
df["rsi14"] = rsi(C, 14)
|
||||
df["atr14"] = atr(H, L, C, 14)
|
||||
m_line, m_sig, _ = macd(C)
|
||||
df["macd"], df["macd_sig"] = m_line, m_sig
|
||||
k, d = stoch_kd(H, L, C, 14, 3)
|
||||
df["stoch_k"], df["stoch_d"] = k, d
|
||||
mid, up, dn = bollinger(C, 20, 2.0)
|
||||
df["bb_mid"], df["bb_up"], df["bb_dn"] = mid, up, dn
|
||||
adx_, pdi, mdi = adx_val(H, L, C, 14)
|
||||
df["adx"], df["pdi"], df["mdi"] = adx_, pdi, mdi
|
||||
df["supertrend"] = supertrend(H, L, C, 10, 3.0)
|
||||
return df
|
||||
|
||||
14
install.sh
14
install.sh
@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
echo "OK. Start:"
|
||||
echo "source venv/bin/activate && python app.py"
|
||||
|
||||
|
||||
#chmod +x install.sh
|
||||
#./install.sh
|
||||
#source venv/bin/activate
|
||||
#nohup python app.py > output.log 2>&1 &
|
||||
37
io_utils.py
37
io_utils.py
@ -1,37 +0,0 @@
|
||||
from __future__ import annotations
|
||||
import os, json
|
||||
import pandas as pd
|
||||
from typing import List
|
||||
from metrics import portfolio_metrics
|
||||
|
||||
def ensure_dirs(root_dir: str, tickers: List[str]):
|
||||
os.makedirs(root_dir, exist_ok=True)
|
||||
for tk in tickers:
|
||||
os.makedirs(os.path.join(root_dir, tk), exist_ok=True)
|
||||
|
||||
def save_outputs(root_dir: str, tickers: List[str], trades: pd.DataFrame, portfolio_eq: pd.DataFrame, cash: float):
|
||||
ensure_dirs(root_dir, tickers)
|
||||
|
||||
if not trades.empty:
|
||||
for tk, tdf in trades.groupby("ticker"):
|
||||
tdf2 = tdf.copy()
|
||||
tdf2["datetime"] = pd.to_datetime(tdf2["time"], unit="ms", utc=True)
|
||||
tdf2.to_csv(os.path.join(root_dir, tk, f"{tk}_trades.csv"), index=False)
|
||||
|
||||
if not portfolio_eq.empty:
|
||||
portfolio_eq["datetime"] = pd.to_datetime(portfolio_eq["time"], unit="ms", utc=True)
|
||||
portfolio_eq.to_csv(os.path.join(root_dir, "portfolio_equity.csv"), index=False)
|
||||
|
||||
eq_series = portfolio_eq.set_index("time")["equity"] if not portfolio_eq.empty else pd.Series(dtype=float)
|
||||
metrics = portfolio_metrics(eq_series) if not eq_series.empty else {"max_dd":0.0,"sharpe":0.0,"cagr":0.0}
|
||||
|
||||
summary = {
|
||||
"ending_cash": round(cash, 2),
|
||||
"n_open_positions": int(trades[trades["action"]=="OPEN"]["ticker"].nunique()) if not trades.empty else 0,
|
||||
"n_trades_closed": int((trades["action"]=="CLOSE").sum()) if not trades.empty else 0,
|
||||
"max_drawdown": metrics["max_dd"],
|
||||
"sharpe_annualized": metrics["sharpe"],
|
||||
"cagr": metrics["cagr"],
|
||||
}
|
||||
with open(os.path.join(root_dir, "portfolio_summary.txt"), "w", encoding="utf-8") as f:
|
||||
json.dump(summary, f, indent=2, ensure_ascii=False)
|
||||
26
metrics.py
26
metrics.py
@ -1,26 +0,0 @@
|
||||
from __future__ import annotations
|
||||
import math
|
||||
import pandas as pd
|
||||
from typing import Dict
|
||||
|
||||
def portfolio_metrics(equity: pd.Series) -> Dict[str, float]:
|
||||
if equity.empty or equity.iloc[0] <= 0:
|
||||
return {"max_dd": 0.0, "sharpe": 0.0, "cagr": 0.0}
|
||||
|
||||
roll_max = equity.cummax()
|
||||
dd = (equity/roll_max) - 1.0
|
||||
max_dd = float(dd.min())
|
||||
|
||||
rets = equity.pct_change().dropna()
|
||||
if rets.empty or rets.std(ddof=0) == 0:
|
||||
sharpe = 0.0
|
||||
else:
|
||||
periods_per_year = 365*24*60 # dla 1m
|
||||
sharpe = float((rets.mean()/rets.std(ddof=0)) * math.sqrt(periods_per_year))
|
||||
|
||||
t0 = pd.to_datetime(equity.index[0], unit="ms", utc=True)
|
||||
t1 = pd.to_datetime(equity.index[-1], unit="ms", utc=True)
|
||||
years = max((t1 - t0).total_seconds() / (365.25*24*3600), 1e-9)
|
||||
cagr = float((equity.iloc[-1]/equity.iloc[0])**(1/years) - 1.0)
|
||||
|
||||
return {"max_dd": max_dd, "sharpe": sharpe, "cagr": cagr}
|
||||
61
new/app.py
61
new/app.py
@ -1,61 +0,0 @@
|
||||
from __future__ import annotations
|
||||
import os, json, threading
|
||||
from flask import Flask, jsonify, render_template, abort
|
||||
from trading_bot import main as trading_bot_main # zakładam, że tak nazywasz plik
|
||||
|
||||
DATA_DIR = os.environ.get("DATA_DIR", ".")
|
||||
|
||||
def _load_json(name: str):
|
||||
path = os.path.join(DATA_DIR, name)
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
try:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
app = Flask(
|
||||
__name__,
|
||||
static_folder="static",
|
||||
static_url_path="/static",
|
||||
template_folder="templates"
|
||||
)
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html")
|
||||
|
||||
@app.route("/api/snapshot")
|
||||
def api_snapshot():
|
||||
data = _load_json("snapshot.json")
|
||||
if data is None: abort(404, "snapshot.json not found")
|
||||
return jsonify(data)
|
||||
|
||||
@app.route("/api/history")
|
||||
def api_history():
|
||||
data = _load_json("portfolio_history.json")
|
||||
if data is None: abort(404, "portfolio_history.json not found")
|
||||
return jsonify(data)
|
||||
|
||||
@app.route("/api/positions")
|
||||
def api_positions():
|
||||
data = _load_json("positions.json")
|
||||
if data is None: data = []
|
||||
return jsonify(data)
|
||||
|
||||
@app.route("/api/trades")
|
||||
def api_trades():
|
||||
data = _load_json("trade_log.json")
|
||||
if data is None: data = []
|
||||
return jsonify(data)
|
||||
|
||||
def start_trading_bot():
|
||||
"""Uruchamia bota w osobnym wątku."""
|
||||
trading_thread = threading.Thread(target=trading_bot_main, daemon=True)
|
||||
trading_thread.start()
|
||||
print("[Flask] Trading bot uruchomiony w wątku.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
start_trading_bot()
|
||||
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)), debug=True)
|
||||
@ -1,125 +0,0 @@
|
||||
# indicators.py
|
||||
from __future__ import annotations
|
||||
import math
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
# ===== Helper =====
|
||||
def _as_1d(a) -> np.ndarray:
|
||||
return np.asarray(a, dtype=float).ravel()
|
||||
|
||||
# ===== Indicators =====
|
||||
def sma(arr: np.ndarray, period: int) -> np.ndarray:
|
||||
arr = _as_1d(arr); n = len(arr)
|
||||
if n == 0: return np.array([], float)
|
||||
if n < period: return np.full(n, np.nan)
|
||||
w = np.ones(period) / period
|
||||
out = np.convolve(arr, w, mode="valid")
|
||||
return np.concatenate([np.full(period-1, np.nan), out])
|
||||
|
||||
def ema(arr: np.ndarray, period: int) -> np.ndarray:
|
||||
arr = _as_1d(arr); n = len(arr)
|
||||
out = np.full(n, np.nan, float)
|
||||
if n == 0: return out
|
||||
k = 2/(period+1); e = np.nan
|
||||
for i, x in enumerate(arr):
|
||||
e = x if (isinstance(e, float) and math.isnan(e)) else x*k + e*(1-k)
|
||||
out[i] = e
|
||||
return out
|
||||
|
||||
def rsi(arr: np.ndarray, period: int = 14) -> np.ndarray:
|
||||
arr = _as_1d(arr); n = len(arr)
|
||||
if n == 0: return np.array([], float)
|
||||
if n < period+1: return np.full(n, np.nan)
|
||||
d = np.diff(arr, prepend=arr[0])
|
||||
g = np.where(d > 0, d, 0.0)
|
||||
L = np.where(d < 0, -d, 0.0)
|
||||
ag, al = ema(g, period), ema(L, period)
|
||||
rs = ag / (al + 1e-9)
|
||||
return 100 - (100 / (1 + rs))
|
||||
|
||||
def atr(H: np.ndarray, L: np.ndarray, C: np.ndarray, period: int = 14) -> np.ndarray:
|
||||
H, L, C = _as_1d(H), _as_1d(L), _as_1d(C); n = len(C)
|
||||
if n == 0: return np.array([], float)
|
||||
if n < period+1: return np.full(n, np.nan)
|
||||
pc = np.roll(C, 1)
|
||||
tr = np.maximum.reduce([H - L, np.abs(H - pc), np.abs(L - pc)])
|
||||
tr[0] = H[0] - L[0]
|
||||
return ema(tr, period)
|
||||
|
||||
def macd(arr: np.ndarray, fast=12, slow=26, signal=9):
|
||||
ef, es = ema(arr, fast), ema(arr, slow)
|
||||
line = ef - es
|
||||
sig = ema(line, signal)
|
||||
hist = line - sig
|
||||
return line, sig, hist
|
||||
|
||||
def stoch_kd(H: np.ndarray, L: np.ndarray, C: np.ndarray, period=14, smooth=3):
|
||||
H, L, C = _as_1d(H), _as_1d(L), _as_1d(C); n = len(C)
|
||||
if n == 0:
|
||||
z = np.array([], float); return z, z
|
||||
if n < period:
|
||||
return np.full(n, np.nan), np.full(n, np.nan)
|
||||
lowest = pd.Series(L).rolling(period, min_periods=1).min().to_numpy()
|
||||
highest = pd.Series(H).rolling(period, min_periods=1).max().to_numpy()
|
||||
k = 100 * (C - lowest) / (highest - lowest + 1e-9)
|
||||
d = pd.Series(k).rolling(smooth, min_periods=1).mean().to_numpy()
|
||||
return k, d
|
||||
|
||||
def bollinger(arr: np.ndarray, period=20, dev=2.0):
|
||||
arr = _as_1d(arr); n = len(arr)
|
||||
if n == 0:
|
||||
z = np.array([], float); return z, z, z
|
||||
s = pd.Series(arr)
|
||||
ma = s.rolling(period, min_periods=1).mean().to_numpy()
|
||||
sd = s.rolling(period, min_periods=1).std(ddof=0).to_numpy()
|
||||
return ma, ma + dev*sd, ma - dev*sd
|
||||
|
||||
def adx_val(H: np.ndarray, L: np.ndarray, C: np.ndarray, period=14):
|
||||
H, L, C = _as_1d(H), _as_1d(L), _as_1d(C); n = len(C)
|
||||
if n == 0:
|
||||
z = np.array([], float); return z, z, z
|
||||
up_move = H - np.roll(H, 1); down_move = np.roll(L, 1) - L
|
||||
up_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0.0)
|
||||
down_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0.0)
|
||||
tr1 = H - L; tr2 = np.abs(H - np.roll(C, 1)); tr3 = np.abs(L - np.roll(C, 1))
|
||||
tr = np.maximum.reduce([tr1, tr2, tr3]); tr[0] = tr1[0]
|
||||
atr14 = ema(tr, period)
|
||||
pdi = 100 * ema(up_dm, period) / (atr14 + 1e-9)
|
||||
mdi = 100 * ema(down_dm, period) / (atr14 + 1e-9)
|
||||
dx = 100 * np.abs(pdi - mdi) / (pdi + mdi + 1e-9)
|
||||
adx = ema(dx, period)
|
||||
return adx, pdi, mdi
|
||||
|
||||
def supertrend(H: np.ndarray, L: np.ndarray, C: np.ndarray, period=10, mult=3.0):
|
||||
H, L, C = _as_1d(H), _as_1d(L), _as_1d(C); n = len(C)
|
||||
if n == 0: return np.array([], float)
|
||||
a = atr(H, L, C, period).copy()
|
||||
hl2 = (H + L) / 2.0
|
||||
ub = hl2 + mult * a
|
||||
lb = hl2 - mult * a
|
||||
st = np.full(n, np.nan)
|
||||
for i in range(1, n):
|
||||
prev = st[i-1] if not np.isnan(st[i-1]) else hl2[i-1]
|
||||
st[i] = ub[i] if C[i-1] <= prev else lb[i]
|
||||
return st
|
||||
|
||||
# ===== Pipeline: add_indicators =====
|
||||
def add_indicators(df: pd.DataFrame, min_bars: int = 200) -> pd.DataFrame:
|
||||
if len(df) < min_bars:
|
||||
raise ValueError(f"Too few bars: {len(df)} < {min_bars}")
|
||||
C, H, L = df["close"].to_numpy(), df["high"].to_numpy(), df["low"].to_numpy()
|
||||
df["ema20"] = ema(C, 20)
|
||||
df["ema50"] = ema(C, 50)
|
||||
df["rsi14"] = rsi(C, 14)
|
||||
df["atr14"] = atr(H, L, C, 14)
|
||||
m_line, m_sig, _ = macd(C)
|
||||
df["macd"], df["macd_sig"] = m_line, m_sig
|
||||
k, d = stoch_kd(H, L, C, 14, 3)
|
||||
df["stoch_k"], df["stoch_d"] = k, d
|
||||
mid, up, dn = bollinger(C, 20, 2.0)
|
||||
df["bb_mid"], df["bb_up"], df["bb_dn"] = mid, up, dn
|
||||
adx_, pdi, mdi = adx_val(H, L, C, 14)
|
||||
df["adx"], df["pdi"], df["mdi"] = adx_, pdi, mdi
|
||||
df["supertrend"] = supertrend(H, L, C, 10, 3.0)
|
||||
return df
|
||||
BIN
new/new.zip
BIN
new/new.zip
Binary file not shown.
333
new/portfolio.py
333
new/portfolio.py
@ -1,333 +0,0 @@
|
||||
# portfolio.py
|
||||
from __future__ import annotations
|
||||
import math, time, json
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
# ========= TRYB KSIĘGOWOŚCI =========
|
||||
# "stock" – akcje (short dodaje gotówkę przy otwarciu, przy zamknięciu oddajesz notional)
|
||||
# "margin" – FX/CFD (short NIE zmienia cash przy otwarciu; cash zmienia się o zrealizowany PnL przy zamknięciu)
|
||||
ACCOUNTING_MODE = "margin"
|
||||
|
||||
# ========= RYZYKO / ATR =========
|
||||
USE_ATR = True # jeśli sygnał ma "atr" > 0, to SL/TP liczone od ATR
|
||||
SL_ATR_MULT = 2.0 # SL = 2.0 * ATR
|
||||
TP_ATR_MULT = 3.0 # TP = 3.0 * ATR
|
||||
RISK_FRACTION = 0.003 # 0.3% equity na trade (position sizing wg 1R)
|
||||
|
||||
# fallback procentowy, gdy brak ATR
|
||||
SL_PCT = 0.010
|
||||
TP_PCT = 0.020
|
||||
TRAIL_PCT = 0.020 # spokojniejszy trailing
|
||||
|
||||
SYMBOL_OVERRIDES = {
|
||||
# "EURUSD=X": {"SL_PCT": 0.0025, "TP_PCT": 0.0050, "TRAIL_PCT": 0.0030},
|
||||
}
|
||||
|
||||
# ========= LIMITY BUDŻETOWE =========
|
||||
ALLOC_FRACTION = 0.25 # max % gotówki na JEDEN LONG
|
||||
MIN_TRADE_CASH = 25.0
|
||||
MAX_NEW_POSITIONS_PER_CYCLE = 2 # None aby wyłączyć limit
|
||||
|
||||
# =================================
|
||||
def _to_native(obj: Any):
|
||||
"""JSON-safe: NaN/Inf -> None; rekurencyjnie czyści struktury."""
|
||||
if isinstance(obj, float):
|
||||
return None if (math.isnan(obj) or math.isinf(obj)) else obj
|
||||
if isinstance(obj, (int, str)) or obj is None:
|
||||
return obj
|
||||
if isinstance(obj, dict):
|
||||
return {k: _to_native(v) for k, v in obj.items()}
|
||||
if isinstance(obj, (list, tuple)):
|
||||
return [_to_native(x) for x in obj]
|
||||
try:
|
||||
return float(obj)
|
||||
except Exception:
|
||||
return str(obj)
|
||||
|
||||
def save_json(path: str, data: Any) -> None:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(_to_native(data), f, ensure_ascii=False, indent=2, allow_nan=False)
|
||||
|
||||
class Portfolio:
|
||||
"""
|
||||
LONG open: cash -= qty*price ; close: cash += qty*price ; PnL = (px - entry)*qty
|
||||
SHORT open (stock): cash += qty*price ; close: cash -= qty*price ; PnL = (entry - px)*qty
|
||||
SHORT open (margin): cash bez zmian ; close: cash += PnL ; PnL = (entry - px)*qty
|
||||
|
||||
total_value:
|
||||
- stock -> cash + positions_net (wartość likwidacyjna)
|
||||
- margin -> start_capital + realized_pnl + unrealized_pnl
|
||||
"""
|
||||
def __init__(self, capital: float):
|
||||
self.cash = float(capital)
|
||||
self.start_capital = float(capital)
|
||||
self.positions: Dict[str, Dict] = {}
|
||||
self.history: List[Dict] = []
|
||||
self.trade_log: List[Dict] = []
|
||||
self.realized_pnl: float = 0.0
|
||||
self.last_prices: Dict[str, float] = {} # ostatnie znane ceny
|
||||
|
||||
# ---------- helpers ----------
|
||||
def _get_params(self, ticker: str):
|
||||
o = SYMBOL_OVERRIDES.get(ticker, {})
|
||||
sl = float(o.get("SL_PCT", SL_PCT))
|
||||
tp = float(o.get("TP_PCT", TP_PCT))
|
||||
tr = float(o.get("TRAIL_PCT", TRAIL_PCT))
|
||||
return sl, tp, tr
|
||||
|
||||
def _init_risk(self, ticker: str, entry: float, side: int, atr: Optional[float] = None):
|
||||
"""Zwraca: stop, take, trail_best, trail_stop, one_r."""
|
||||
sl_pct, tp_pct, trail_pct = self._get_params(ticker)
|
||||
use_atr = USE_ATR and atr is not None and isinstance(atr, (int, float)) and atr > 0.0
|
||||
|
||||
if use_atr:
|
||||
sl_dist = SL_ATR_MULT * float(atr)
|
||||
tp_dist = TP_ATR_MULT * float(atr)
|
||||
stop = entry - sl_dist if side == 1 else entry + sl_dist
|
||||
take = entry + tp_dist if side == 1 else entry - tp_dist
|
||||
one_r = sl_dist
|
||||
else:
|
||||
if side == 1:
|
||||
stop = entry * (1.0 - sl_pct); take = entry * (1.0 + tp_pct)
|
||||
else:
|
||||
stop = entry * (1.0 + sl_pct); take = entry * (1.0 - tp_pct)
|
||||
one_r = abs(entry - stop)
|
||||
|
||||
trail_best = entry
|
||||
trail_stop = stop
|
||||
return stop, take, trail_best, trail_stop, max(one_r, 1e-8)
|
||||
|
||||
def _update_trailing(self, ticker: str, pos: Dict, price: float):
|
||||
"""Breakeven po 1R + trailing procentowy."""
|
||||
_, _, trail_pct = self._get_params(ticker)
|
||||
|
||||
# BE po 1R
|
||||
if pos["side"] == 1 and not pos.get("be_done", False):
|
||||
if price >= pos["entry"] + pos["one_r"]:
|
||||
pos["stop"] = max(pos["stop"], pos["entry"])
|
||||
pos["be_done"] = True
|
||||
elif pos["side"] == -1 and not pos.get("be_done", False):
|
||||
if price <= pos["entry"] - pos["one_r"]:
|
||||
pos["stop"] = min(pos["stop"], pos["entry"])
|
||||
pos["be_done"] = True
|
||||
|
||||
# trailing
|
||||
if pos["side"] == 1:
|
||||
if price > pos["trail_best"]:
|
||||
pos["trail_best"] = price
|
||||
pos["trail_stop"] = pos["trail_best"] * (1.0 - trail_pct)
|
||||
else:
|
||||
if price < pos["trail_best"]:
|
||||
pos["trail_best"] = price
|
||||
pos["trail_stop"] = pos["trail_best"] * (1.0 + trail_pct)
|
||||
|
||||
def _positions_values(self, prices: Optional[Dict[str, float]] = None) -> Dict[str, float]:
|
||||
"""Zwraca Σ|qty*px| oraz Σqty*px*side. Fallback ceny: prices -> self.last_prices -> entry."""
|
||||
gross = 0.0
|
||||
net = 0.0
|
||||
for t, p in self.positions.items():
|
||||
px = None
|
||||
if prices is not None:
|
||||
px = prices.get(t)
|
||||
if px is None or (isinstance(px, float) and math.isnan(px)):
|
||||
px = self.last_prices.get(t)
|
||||
if px is None or (isinstance(px, float) and math.isnan(px)):
|
||||
px = p["entry"]
|
||||
gross += abs(p["qty"] * px)
|
||||
net += p["qty"] * px * p["side"]
|
||||
return {"positions_gross": gross, "positions_net": net}
|
||||
|
||||
def unrealized_pnl(self, prices: Dict[str, float]) -> float:
|
||||
"""Σ side * (px - entry) * qty."""
|
||||
upnl = 0.0
|
||||
for t, p in self.positions.items():
|
||||
px = prices.get(t, self.last_prices.get(t, p["entry"]))
|
||||
if px is None or (isinstance(px, float) and math.isnan(px)): px = p["entry"]
|
||||
upnl += p["side"] * (px - p["entry"]) * p["qty"]
|
||||
return upnl
|
||||
|
||||
def equity_from_start(self, prices: Dict[str, float]) -> float:
|
||||
return self.start_capital + self.realized_pnl + self.unrealized_pnl(prices)
|
||||
|
||||
# ---------- zamknięcia / log ----------
|
||||
def _close_position(self, ticker: str, price: float, reason: str):
|
||||
pos = self.positions.get(ticker)
|
||||
if not pos: return
|
||||
qty, entry, side = pos["qty"], pos["entry"], pos["side"]
|
||||
|
||||
if side == 1:
|
||||
self.cash += qty * price
|
||||
pnl_abs = (price - entry) * qty
|
||||
else:
|
||||
pnl_abs = (entry - price) * qty
|
||||
if ACCOUNTING_MODE == "stock":
|
||||
self.cash -= qty * price
|
||||
else:
|
||||
self.cash += pnl_abs # margin: tylko PnL wpływa na cash
|
||||
|
||||
denom = max(qty * entry, 1e-12)
|
||||
pnl_pct = pnl_abs / denom
|
||||
self.realized_pnl += pnl_abs
|
||||
|
||||
self._log_trade("SELL", ticker, price, qty, side, pnl_abs, pnl_pct, reason=reason)
|
||||
del self.positions[ticker]
|
||||
|
||||
def _log_trade(self, action: str, ticker: str, price: float, qty: float, side: int,
|
||||
pnl_abs: float = 0.0, pnl_pct: float = 0.0, reason: Optional[str] = None):
|
||||
ts = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if action == "BUY":
|
||||
print(f"[{ts}] BUY {ticker} @ {price:.6f}, qty={qty:.6f}, side={'LONG' if side==1 else 'SHORT'}, cash={self.cash:.2f}")
|
||||
else:
|
||||
r = f", reason={reason}" if reason else ""
|
||||
print(f"[{ts}] SELL {ticker} @ {price:.6f}, qty={qty:.6f}, PnL={pnl_abs:+.2f} ({pnl_pct:+.2%}), cash={self.cash:.2f}{r}")
|
||||
self.trade_log.append({
|
||||
"time": ts, "action": action, "ticker": ticker, "price": float(price),
|
||||
"qty": float(qty), "side": int(side),
|
||||
"pnl_abs": float(pnl_abs), "pnl_pct": float(pnl_pct),
|
||||
"realized_pnl_cum": float(self.realized_pnl), "cash_after": float(self.cash),
|
||||
"reason": reason or ""
|
||||
})
|
||||
|
||||
# ---------- główna aktualizacja ----------
|
||||
def on_signals(self, sigs: List[dict]):
|
||||
# normalizacja
|
||||
clean: List[Dict[str, Any]] = []
|
||||
for s in sigs:
|
||||
if isinstance(s, dict):
|
||||
px = s.get("price")
|
||||
if s.get("error") is None and px is not None and not math.isnan(px):
|
||||
clean.append(s)
|
||||
else:
|
||||
if getattr(s, "error", None) is None and not math.isnan(getattr(s, "price", float("nan"))):
|
||||
clean.append({
|
||||
"ticker": s.ticker, "price": s.price, "signal": s.signal, "time": s.time,
|
||||
"atr": getattr(s, "atr", None)
|
||||
})
|
||||
|
||||
# snapshot, gdy brak świeżych danych
|
||||
if not clean:
|
||||
vals = self._positions_values(None)
|
||||
prices_map = {t: self.last_prices.get(t, p["entry"]) for t, p in self.positions.items()}
|
||||
if ACCOUNTING_MODE == "stock":
|
||||
account_value = self.cash + vals["positions_net"]
|
||||
else:
|
||||
account_value = self.equity_from_start(prices_map)
|
||||
unreal = self.unrealized_pnl(prices_map)
|
||||
self.history.append({
|
||||
"time": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"cash": float(self.cash),
|
||||
"positions_value": float(vals["positions_gross"]),
|
||||
"positions_net": float(vals["positions_net"]),
|
||||
"total_value": float(account_value),
|
||||
"equity_from_start": float(self.start_capital + self.realized_pnl + unreal),
|
||||
"unrealized_pnl": float(unreal),
|
||||
"realized_pnl_cum": float(self.realized_pnl),
|
||||
"open_positions": int(len(self.positions))
|
||||
})
|
||||
save_json("portfolio_history.json", self.history)
|
||||
return
|
||||
|
||||
# mapy cen
|
||||
prices = {s["ticker"]: float(s["price"]) for s in clean}
|
||||
self.last_prices.update(prices)
|
||||
|
||||
# 1) risk exits (SL/TP/TRAIL + BE)
|
||||
to_close = []
|
||||
for t, pos in list(self.positions.items()):
|
||||
price = prices.get(t)
|
||||
if price is None or math.isnan(price): continue
|
||||
self._update_trailing(t, pos, price)
|
||||
if pos["side"] == 1:
|
||||
if price <= pos["stop"]: to_close.append((t, "SL"))
|
||||
elif price >= pos["take"]: to_close.append((t, "TP"))
|
||||
elif price <= pos.get("trail_stop", -float("inf")): to_close.append((t, "TRAIL"))
|
||||
else:
|
||||
if price >= pos["stop"]: to_close.append((t, "SL"))
|
||||
elif price <= pos["take"]: to_close.append((t, "TP"))
|
||||
elif price >= pos.get("trail_stop", float("inf")): to_close.append((t, "TRAIL"))
|
||||
for t, reason in to_close:
|
||||
price = prices.get(t)
|
||||
if price is not None and not math.isnan(price) and t in self.positions:
|
||||
self._close_position(t, price, reason=reason)
|
||||
|
||||
# 2) zamknięcie na przeciwny/HOLD
|
||||
for s in clean:
|
||||
t, sig = s["ticker"], int(s["signal"])
|
||||
price = float(s["price"])
|
||||
if t in self.positions:
|
||||
pos = self.positions[t]
|
||||
if sig == 0 or sig != pos["side"]:
|
||||
self._close_position(t, price, reason="SIGNAL")
|
||||
|
||||
# 3) otwarcia – ATR sizing + limity
|
||||
candidates = [s for s in clean if s["ticker"] not in self.positions and int(s["signal"]) != 0]
|
||||
if MAX_NEW_POSITIONS_PER_CYCLE and MAX_NEW_POSITIONS_PER_CYCLE > 0:
|
||||
candidates = candidates[:MAX_NEW_POSITIONS_PER_CYCLE]
|
||||
|
||||
for s in candidates:
|
||||
t, sig, price = s["ticker"], int(s["signal"]), float(s["price"])
|
||||
atr = s.get("atr", None)
|
||||
if price <= 0: continue
|
||||
|
||||
stop, take, trail_best, trail_stop, one_r = self._init_risk(t, price, sig, atr)
|
||||
|
||||
# equity teraz (dla sizingu ryzyka)
|
||||
if ACCOUNTING_MODE == "margin":
|
||||
equity_now = self.start_capital + self.realized_pnl + self.unrealized_pnl(self.last_prices)
|
||||
else:
|
||||
vals_tmp = self._positions_values(self.last_prices)
|
||||
equity_now = self.cash + vals_tmp["positions_net"]
|
||||
|
||||
risk_amount = max(1e-6, equity_now * RISK_FRACTION)
|
||||
qty_risk = risk_amount / max(one_r, 1e-8)
|
||||
|
||||
# limit gotówki dla LONG
|
||||
per_trade_cash = max(MIN_TRADE_CASH, self.cash * ALLOC_FRACTION)
|
||||
qty_cash = per_trade_cash / max(price, 1e-12) if sig == 1 else float("inf")
|
||||
|
||||
qty = max(0.0, min(qty_risk, qty_cash))
|
||||
if qty <= 0: continue
|
||||
|
||||
# księgowanie gotówki przy otwarciu
|
||||
if sig == 1:
|
||||
cost = qty * price
|
||||
if self.cash < cost: continue
|
||||
self.cash -= cost
|
||||
else:
|
||||
if ACCOUNTING_MODE == "stock":
|
||||
self.cash += qty * price
|
||||
# margin: brak zmiany cash przy short open
|
||||
|
||||
self.positions[t] = {
|
||||
"qty": qty, "entry": price, "side": sig,
|
||||
"stop": stop, "take": take,
|
||||
"trail_best": trail_best, "trail_stop": trail_stop,
|
||||
"one_r": one_r, "be_done": False
|
||||
}
|
||||
self._log_trade("BUY" if sig == 1 else "SELL", t, price, qty, sig)
|
||||
|
||||
# 4) snapshot na bieżących/ostatnich cenach
|
||||
vals = self._positions_values(self.last_prices)
|
||||
prices_map = {t: self.last_prices.get(t, p["entry"]) for t, p in self.positions.items()}
|
||||
if ACCOUNTING_MODE == "stock":
|
||||
account_value = self.cash + vals["positions_net"]
|
||||
else:
|
||||
account_value = self.equity_from_start(prices_map)
|
||||
unreal = self.unrealized_pnl(prices_map)
|
||||
|
||||
self.history.append({
|
||||
"time": clean[0]["time"],
|
||||
"cash": float(self.cash),
|
||||
"positions_value": float(vals["positions_gross"]),
|
||||
"positions_net": float(vals["positions_net"]),
|
||||
"total_value": float(account_value),
|
||||
"equity_from_start": float(self.start_capital + self.realized_pnl + unreal),
|
||||
"unrealized_pnl": float(unreal),
|
||||
"realized_pnl_cum": float(self.realized_pnl),
|
||||
"open_positions": int(len(self.positions))
|
||||
})
|
||||
|
||||
# 5) zapisy
|
||||
save_json("trade_log.json", self.trade_log)
|
||||
save_json("portfolio_history.json", self.history)
|
||||
save_json("positions.json", [{"ticker": t, **p} for t, p in self.positions.items()])
|
||||
@ -1,79 +0,0 @@
|
||||
[
|
||||
{
|
||||
"time": "2025-08-15 10:11:00+00:00",
|
||||
"cash": 10000.0,
|
||||
"positions_value": 0.0,
|
||||
"positions_net": 0.0,
|
||||
"total_value": 10000.0,
|
||||
"equity_from_start": 10000.0,
|
||||
"unrealized_pnl": 0.0,
|
||||
"realized_pnl_cum": 0.0,
|
||||
"open_positions": 0
|
||||
},
|
||||
{
|
||||
"time": "2025-08-15 10:12:00+00:00",
|
||||
"cash": 10000.0,
|
||||
"positions_value": 0.0,
|
||||
"positions_net": 0.0,
|
||||
"total_value": 10000.0,
|
||||
"equity_from_start": 10000.0,
|
||||
"unrealized_pnl": 0.0,
|
||||
"realized_pnl_cum": 0.0,
|
||||
"open_positions": 0
|
||||
},
|
||||
{
|
||||
"time": "2025-08-15 10:13:00+00:00",
|
||||
"cash": 10000.0,
|
||||
"positions_value": 0.0,
|
||||
"positions_net": 0.0,
|
||||
"total_value": 10000.0,
|
||||
"equity_from_start": 10000.0,
|
||||
"unrealized_pnl": 0.0,
|
||||
"realized_pnl_cum": 0.0,
|
||||
"open_positions": 0
|
||||
},
|
||||
{
|
||||
"time": "2025-08-15 10:14:00+00:00",
|
||||
"cash": 10000.0,
|
||||
"positions_value": 0.0,
|
||||
"positions_net": 0.0,
|
||||
"total_value": 10000.0,
|
||||
"equity_from_start": 10000.0,
|
||||
"unrealized_pnl": 0.0,
|
||||
"realized_pnl_cum": 0.0,
|
||||
"open_positions": 0
|
||||
},
|
||||
{
|
||||
"time": "2025-08-15 10:15:00+00:00",
|
||||
"cash": 10000.0,
|
||||
"positions_value": 0.0,
|
||||
"positions_net": 0.0,
|
||||
"total_value": 10000.0,
|
||||
"equity_from_start": 10000.0,
|
||||
"unrealized_pnl": 0.0,
|
||||
"realized_pnl_cum": 0.0,
|
||||
"open_positions": 0
|
||||
},
|
||||
{
|
||||
"time": "2025-08-15 10:16:00+00:00",
|
||||
"cash": 10000.0,
|
||||
"positions_value": 5999.999999999975,
|
||||
"positions_net": -5999.999999999975,
|
||||
"total_value": 10000.0,
|
||||
"equity_from_start": 10000.0,
|
||||
"unrealized_pnl": 0.0,
|
||||
"realized_pnl_cum": 0.0,
|
||||
"open_positions": 2
|
||||
},
|
||||
{
|
||||
"time": "2025-08-15 10:17:00+00:00",
|
||||
"cash": 10000.0,
|
||||
"positions_value": 5999.999999999975,
|
||||
"positions_net": -5999.999999999975,
|
||||
"total_value": 10000.0,
|
||||
"equity_from_start": 10000.0,
|
||||
"unrealized_pnl": 0.0,
|
||||
"realized_pnl_cum": 0.0,
|
||||
"open_positions": 2
|
||||
}
|
||||
]
|
||||
@ -1,26 +0,0 @@
|
||||
[
|
||||
{
|
||||
"ticker": "OP-USD",
|
||||
"qty": 3973.896468582607,
|
||||
"entry": 0.7549265623092651,
|
||||
"side": -1,
|
||||
"stop": 0.7624758279323578,
|
||||
"take": 0.7398280310630798,
|
||||
"trail_best": 0.7549265623092651,
|
||||
"trail_stop": 0.7624758279323578,
|
||||
"one_r": 0.00754926562309266,
|
||||
"be_done": false
|
||||
},
|
||||
{
|
||||
"ticker": "NEAR-USD",
|
||||
"qty": 1076.5240904642392,
|
||||
"entry": 2.7867467403411865,
|
||||
"side": -1,
|
||||
"stop": 2.8146142077445986,
|
||||
"take": 2.731011805534363,
|
||||
"trail_best": 2.7867467403411865,
|
||||
"trail_stop": 2.8146142077445986,
|
||||
"one_r": 0.02786746740341206,
|
||||
"be_done": false
|
||||
}
|
||||
]
|
||||
@ -1,221 +0,0 @@
|
||||
// static/app.js
|
||||
const fmt2 = (x) => (x == null || isNaN(x) ? "—" : Number(x).toFixed(2));
|
||||
const fmt6 = (x) => (x == null || isNaN(x) ? "—" : Number(x).toFixed(6));
|
||||
const fmtPct= (x) => (x == null || isNaN(x) ? "—" : (Number(x) * 100).toFixed(2) + "%");
|
||||
|
||||
async function getJSON(url) {
|
||||
const r = await fetch(url, { cache: "no-store" });
|
||||
if (!r.ok) throw new Error(`${url}: ${r.status}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
// prosty wykres linii na canvas (bez bibliotek)
|
||||
function drawLineChart(canvas, points) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
const pad = 32;
|
||||
const w = canvas.width, h = canvas.height;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
if (!points || points.length === 0) {
|
||||
ctx.fillStyle = "#9aa3b2";
|
||||
ctx.fillText("Brak danych", 10, 20);
|
||||
return;
|
||||
}
|
||||
|
||||
const n = points.length;
|
||||
const ys = points.map(p => p.y);
|
||||
const minY = Math.min(...ys), maxY = Math.max(...ys);
|
||||
const yLo = minY === maxY ? minY - 1 : minY;
|
||||
const yHi = minY === maxY ? maxY + 1 : maxY;
|
||||
|
||||
const x0 = pad, y0 = h - pad, x1 = w - pad, y1 = pad;
|
||||
const xScale = (i) => x0 + (i / (n - 1)) * (x1 - x0);
|
||||
const yScale = (y) => y0 - ((y - yLo) / (yHi - yLo)) * (y0 - y1);
|
||||
|
||||
// osie
|
||||
ctx.strokeStyle = "#242a36";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x0, y0); ctx.lineTo(x1, y0);
|
||||
ctx.moveTo(x0, y0); ctx.lineTo(x0, y1);
|
||||
ctx.stroke();
|
||||
|
||||
// siatka
|
||||
ctx.strokeStyle = "#1b2130";
|
||||
[0.25, 0.5, 0.75].forEach(f => {
|
||||
const yy = y0 - (y0 - y1) * f;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x0, yy); ctx.lineTo(x1, yy);
|
||||
ctx.stroke();
|
||||
});
|
||||
|
||||
// podpisy min/max
|
||||
ctx.fillStyle = "#9aa3b2";
|
||||
ctx.font = "12px system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial";
|
||||
ctx.fillText(fmt2(yHi), 6, y1 + 10);
|
||||
ctx.fillText(fmt2(yLo), 6, y0 - 2);
|
||||
|
||||
// linia
|
||||
ctx.strokeStyle = "#4da3ff";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(xScale(0), yScale(ys[0]));
|
||||
for (let i = 1; i < n; i++) ctx.lineTo(xScale(i), yScale(ys[i]));
|
||||
ctx.stroke();
|
||||
|
||||
// kropka na końcu
|
||||
const lastX = xScale(n - 1), lastY = yScale(ys[n - 1]);
|
||||
ctx.fillStyle = "#e7e9ee";
|
||||
ctx.beginPath(); ctx.arc(lastX, lastY, 3, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.fillText(fmt2(ys[n - 1]), lastX + 6, lastY - 6);
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
try {
|
||||
const [snap, hist, pos, trades] = await Promise.all([
|
||||
getJSON("/api/snapshot"),
|
||||
getJSON("/api/history"),
|
||||
getJSON("/api/positions"),
|
||||
getJSON("/api/trades"),
|
||||
]);
|
||||
|
||||
// czas
|
||||
document.getElementById("last-update").textContent =
|
||||
"Ostatnia aktualizacja: " + (snap?.last_history?.time || "—");
|
||||
|
||||
// ===== ostatni wiersz historii =====
|
||||
const last = Array.isArray(hist) && hist.length ? hist[hist.length - 1] : null;
|
||||
|
||||
const cashVal = Number(last?.cash ?? 0);
|
||||
const totalVal = Number(
|
||||
last?.total_value ??
|
||||
(Number(last?.cash ?? 0) + Number(last?.positions_net ?? 0)) // fallback dla starszych logów
|
||||
);
|
||||
|
||||
// KARTY
|
||||
document.getElementById("cash").textContent = fmt2(cashVal);
|
||||
document.getElementById("total-value").textContent = fmt2(totalVal);
|
||||
|
||||
// mapa ostatnich cen do liczenia zysku (unrealized)
|
||||
const lastPrice = new Map();
|
||||
(snap?.signals || []).forEach(s => {
|
||||
const px = Number(s.price);
|
||||
if (!isNaN(px)) lastPrice.set(s.ticker, px);
|
||||
});
|
||||
|
||||
// Zysk = suma niezrealizowanych PnL na otwartych pozycjach
|
||||
let unrealPnL = 0;
|
||||
(pos || []).forEach(p => {
|
||||
const price = Number(lastPrice.get(p.ticker));
|
||||
const entry = Number(p.entry);
|
||||
const qty = Number(p.qty);
|
||||
const side = Number(p.side);
|
||||
if (isNaN(price) || isNaN(entry) || isNaN(qty) || isNaN(side)) return;
|
||||
unrealPnL += side === 1 ? (price - entry) * qty : (entry - price) * qty;
|
||||
});
|
||||
const unrlEl = document.getElementById("unrealized");
|
||||
unrlEl.textContent = fmt2(unrealPnL);
|
||||
unrlEl.classList.remove("pnl-positive", "pnl-negative");
|
||||
if (unrealPnL > 0) unrlEl.classList.add("pnl-positive");
|
||||
else if (unrealPnL < 0) unrlEl.classList.add("pnl-negative");
|
||||
|
||||
// liczba otwartych pozycji
|
||||
document.getElementById("open-pos").textContent =
|
||||
Number(last?.open_positions ?? (pos?.length ?? 0));
|
||||
|
||||
// ===== WYKRES WARTOŚCI KONTA (TOTAL) =====
|
||||
const totalCanvas = document.getElementById("totalChart");
|
||||
const totalPoints = (hist || [])
|
||||
.map((row, i) => {
|
||||
const v = (row.total_value != null)
|
||||
? Number(row.total_value)
|
||||
: Number(row.cash ?? 0) + Number(row.positions_net ?? 0);
|
||||
return { x: i, y: v };
|
||||
})
|
||||
.filter(p => !isNaN(p.y))
|
||||
.slice(-500);
|
||||
drawLineChart(totalCanvas, totalPoints);
|
||||
|
||||
// ===== WYKRES GOTÓWKI (CASH) =====
|
||||
const cashCanvas = document.getElementById("cashChart");
|
||||
const cashPoints = (hist || [])
|
||||
.map((row, i) => ({ x: i, y: Number(row.cash ?? NaN) }))
|
||||
.filter(p => !isNaN(p.y))
|
||||
.slice(-500);
|
||||
drawLineChart(cashCanvas, cashPoints);
|
||||
|
||||
// ===== POZYCJE =====
|
||||
const posBody = document.querySelector("#positions-table tbody");
|
||||
posBody.innerHTML = "";
|
||||
(pos || []).forEach((p) => {
|
||||
const price = Number(lastPrice.get(p.ticker));
|
||||
const entry = Number(p.entry);
|
||||
const qty = Number(p.qty);
|
||||
const side = Number(p.side);
|
||||
|
||||
let upnl = NaN, upct = NaN;
|
||||
if (!isNaN(price) && !isNaN(entry) && !isNaN(qty) && !isNaN(side)) {
|
||||
upnl = side === 1 ? (price - entry) * qty : (entry - price) * qty;
|
||||
const denom = Math.max(qty * entry, 1e-12);
|
||||
upct = upnl / denom;
|
||||
}
|
||||
const pnlClass = upnl > 0 ? "pnl-positive" : (upnl < 0 ? "pnl-negative" : "");
|
||||
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<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);
|
||||
@ -1,114 +0,0 @@
|
||||
<!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>
|
||||
@ -1,28 +0,0 @@
|
||||
[
|
||||
{
|
||||
"time": "2025-08-15 12:18:01",
|
||||
"action": "SELL",
|
||||
"ticker": "OP-USD",
|
||||
"price": 0.7549265623092651,
|
||||
"qty": 3973.896468582607,
|
||||
"side": -1,
|
||||
"pnl_abs": 0.0,
|
||||
"pnl_pct": 0.0,
|
||||
"realized_pnl_cum": 0.0,
|
||||
"cash_after": 10000.0,
|
||||
"reason": ""
|
||||
},
|
||||
{
|
||||
"time": "2025-08-15 12:18:01",
|
||||
"action": "SELL",
|
||||
"ticker": "NEAR-USD",
|
||||
"price": 2.7867467403411865,
|
||||
"qty": 1076.5240904642392,
|
||||
"side": -1,
|
||||
"pnl_abs": 0.0,
|
||||
"pnl_pct": 0.0,
|
||||
"realized_pnl_cum": 0.0,
|
||||
"cash_after": 10000.0,
|
||||
"reason": ""
|
||||
}
|
||||
]
|
||||
338
portfolio.py
338
portfolio.py
@ -1,11 +1,39 @@
|
||||
# portfolio.py
|
||||
from __future__ import annotations
|
||||
import math, time, json
|
||||
from typing import Dict, List, Any
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
# ========= TRYB KSIĘGOWOŚCI =========
|
||||
# "stock" – akcje (short dodaje gotówkę przy otwarciu, przy zamknięciu oddajesz notional)
|
||||
# "margin" – FX/CFD (short NIE zmienia cash przy otwarciu; cash zmienia się o zrealizowany PnL przy zamknięciu)
|
||||
ACCOUNTING_MODE = "margin"
|
||||
|
||||
# ========= RYZYKO / ATR =========
|
||||
USE_ATR = True # jeśli sygnał ma "atr" > 0, to SL/TP liczone od ATR
|
||||
SL_ATR_MULT = 2.0 # SL = 2.0 * ATR
|
||||
TP_ATR_MULT = 3.0 # TP = 3.0 * ATR
|
||||
RISK_FRACTION = 0.003 # 0.3% equity na trade (position sizing wg 1R)
|
||||
|
||||
# fallback procentowy, gdy brak ATR
|
||||
SL_PCT = 0.010
|
||||
TP_PCT = 0.020
|
||||
TRAIL_PCT = 0.020 # spokojniejszy trailing
|
||||
|
||||
SYMBOL_OVERRIDES = {
|
||||
# "EURUSD=X": {"SL_PCT": 0.0025, "TP_PCT": 0.0050, "TRAIL_PCT": 0.0030},
|
||||
}
|
||||
|
||||
# ========= LIMITY BUDŻETOWE =========
|
||||
ALLOC_FRACTION = 0.25 # max % gotówki na JEDEN LONG
|
||||
MIN_TRADE_CASH = 25.0
|
||||
MAX_NEW_POSITIONS_PER_CYCLE = 2 # None aby wyłączyć limit
|
||||
|
||||
# =================================
|
||||
def _to_native(obj: Any):
|
||||
"""Bezpieczne rzutowanie do typów akceptowalnych przez JSON."""
|
||||
if isinstance(obj, (float, int, str)) or obj is None:
|
||||
"""JSON-safe: NaN/Inf -> None; rekurencyjnie czyści struktury."""
|
||||
if isinstance(obj, float):
|
||||
return None if (math.isnan(obj) or math.isinf(obj)) else obj
|
||||
if isinstance(obj, (int, str)) or obj is None:
|
||||
return obj
|
||||
if isinstance(obj, dict):
|
||||
return {k: _to_native(v) for k, v in obj.items()}
|
||||
@ -18,16 +46,17 @@ def _to_native(obj: Any):
|
||||
|
||||
def save_json(path: str, data: Any) -> None:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(_to_native(data), f, ensure_ascii=False, indent=2)
|
||||
json.dump(_to_native(data), f, ensure_ascii=False, indent=2, allow_nan=False)
|
||||
|
||||
class Portfolio:
|
||||
"""
|
||||
Prosta symulacja portfela:
|
||||
- cash
|
||||
- positions: {ticker: {"qty": float, "entry": float, "side": int}}
|
||||
- equity = cash + niezrealizowany PnL
|
||||
Logi transakcji z realized PnL (wartość i %), plus PnL skumulowany.
|
||||
Zapis tylko w JSON.
|
||||
LONG open: cash -= qty*price ; close: cash += qty*price ; PnL = (px - entry)*qty
|
||||
SHORT open (stock): cash += qty*price ; close: cash -= qty*price ; PnL = (entry - px)*qty
|
||||
SHORT open (margin): cash bez zmian ; close: cash += PnL ; PnL = (entry - px)*qty
|
||||
|
||||
total_value:
|
||||
- stock -> cash + positions_net (wartość likwidacyjna)
|
||||
- margin -> start_capital + realized_pnl + unrealized_pnl
|
||||
"""
|
||||
def __init__(self, capital: float):
|
||||
self.cash = float(capital)
|
||||
@ -35,103 +64,270 @@ class Portfolio:
|
||||
self.positions: Dict[str, Dict] = {}
|
||||
self.history: List[Dict] = []
|
||||
self.trade_log: List[Dict] = []
|
||||
self.realized_pnl: float = 0.0 # skumulowany realized PnL
|
||||
self.realized_pnl: float = 0.0
|
||||
self.last_prices: Dict[str, float] = {} # ostatnie znane ceny
|
||||
|
||||
def mark_to_market(self, prices: Dict[str, float]) -> float:
|
||||
unreal = 0.0
|
||||
# ---------- helpers ----------
|
||||
def _get_params(self, ticker: str):
|
||||
o = SYMBOL_OVERRIDES.get(ticker, {})
|
||||
sl = float(o.get("SL_PCT", SL_PCT))
|
||||
tp = float(o.get("TP_PCT", TP_PCT))
|
||||
tr = float(o.get("TRAIL_PCT", TRAIL_PCT))
|
||||
return sl, tp, tr
|
||||
|
||||
def _init_risk(self, ticker: str, entry: float, side: int, atr: Optional[float] = None):
|
||||
"""Zwraca: stop, take, trail_best, trail_stop, one_r."""
|
||||
sl_pct, tp_pct, trail_pct = self._get_params(ticker)
|
||||
use_atr = USE_ATR and atr is not None and isinstance(atr, (int, float)) and atr > 0.0
|
||||
|
||||
if use_atr:
|
||||
sl_dist = SL_ATR_MULT * float(atr)
|
||||
tp_dist = TP_ATR_MULT * float(atr)
|
||||
stop = entry - sl_dist if side == 1 else entry + sl_dist
|
||||
take = entry + tp_dist if side == 1 else entry - tp_dist
|
||||
one_r = sl_dist
|
||||
else:
|
||||
if side == 1:
|
||||
stop = entry * (1.0 - sl_pct); take = entry * (1.0 + tp_pct)
|
||||
else:
|
||||
stop = entry * (1.0 + sl_pct); take = entry * (1.0 - tp_pct)
|
||||
one_r = abs(entry - stop)
|
||||
|
||||
trail_best = entry
|
||||
trail_stop = stop
|
||||
return stop, take, trail_best, trail_stop, max(one_r, 1e-8)
|
||||
|
||||
def _update_trailing(self, ticker: str, pos: Dict, price: float):
|
||||
"""Breakeven po 1R + trailing procentowy."""
|
||||
_, _, trail_pct = self._get_params(ticker)
|
||||
|
||||
# BE po 1R
|
||||
if pos["side"] == 1 and not pos.get("be_done", False):
|
||||
if price >= pos["entry"] + pos["one_r"]:
|
||||
pos["stop"] = max(pos["stop"], pos["entry"])
|
||||
pos["be_done"] = True
|
||||
elif pos["side"] == -1 and not pos.get("be_done", False):
|
||||
if price <= pos["entry"] - pos["one_r"]:
|
||||
pos["stop"] = min(pos["stop"], pos["entry"])
|
||||
pos["be_done"] = True
|
||||
|
||||
# trailing
|
||||
if pos["side"] == 1:
|
||||
if price > pos["trail_best"]:
|
||||
pos["trail_best"] = price
|
||||
pos["trail_stop"] = pos["trail_best"] * (1.0 - trail_pct)
|
||||
else:
|
||||
if price < pos["trail_best"]:
|
||||
pos["trail_best"] = price
|
||||
pos["trail_stop"] = pos["trail_best"] * (1.0 + trail_pct)
|
||||
|
||||
def _positions_values(self, prices: Optional[Dict[str, float]] = None) -> Dict[str, float]:
|
||||
"""Zwraca Σ|qty*px| oraz Σqty*px*side. Fallback ceny: prices -> self.last_prices -> entry."""
|
||||
gross = 0.0
|
||||
net = 0.0
|
||||
for t, p in self.positions.items():
|
||||
price = prices.get(t)
|
||||
if price is not None and not math.isnan(price):
|
||||
unreal += (price - p["entry"]) * p["qty"] * p["side"]
|
||||
return self.cash + unreal
|
||||
px = None
|
||||
if prices is not None:
|
||||
px = prices.get(t)
|
||||
if px is None or (isinstance(px, float) and math.isnan(px)):
|
||||
px = self.last_prices.get(t)
|
||||
if px is None or (isinstance(px, float) and math.isnan(px)):
|
||||
px = p["entry"]
|
||||
gross += abs(p["qty"] * px)
|
||||
net += p["qty"] * px * p["side"]
|
||||
return {"positions_gross": gross, "positions_net": net}
|
||||
|
||||
def unrealized_pnl(self, prices: Dict[str, float]) -> float:
|
||||
"""Σ side * (px - entry) * qty."""
|
||||
upnl = 0.0
|
||||
for t, p in self.positions.items():
|
||||
px = prices.get(t, self.last_prices.get(t, p["entry"]))
|
||||
if px is None or (isinstance(px, float) and math.isnan(px)): px = p["entry"]
|
||||
upnl += p["side"] * (px - p["entry"]) * p["qty"]
|
||||
return upnl
|
||||
|
||||
def equity_from_start(self, prices: Dict[str, float]) -> float:
|
||||
return self.start_capital + self.realized_pnl + self.unrealized_pnl(prices)
|
||||
|
||||
# ---------- zamknięcia / log ----------
|
||||
def _close_position(self, ticker: str, price: float, reason: str):
|
||||
pos = self.positions.get(ticker)
|
||||
if not pos: return
|
||||
qty, entry, side = pos["qty"], pos["entry"], pos["side"]
|
||||
|
||||
if side == 1:
|
||||
self.cash += qty * price
|
||||
pnl_abs = (price - entry) * qty
|
||||
else:
|
||||
pnl_abs = (entry - price) * qty
|
||||
if ACCOUNTING_MODE == "stock":
|
||||
self.cash -= qty * price
|
||||
else:
|
||||
self.cash += pnl_abs # margin: tylko PnL wpływa na cash
|
||||
|
||||
denom = max(qty * entry, 1e-12)
|
||||
pnl_pct = pnl_abs / denom
|
||||
self.realized_pnl += pnl_abs
|
||||
|
||||
self._log_trade("SELL", ticker, price, qty, side, pnl_abs, pnl_pct, reason=reason)
|
||||
del self.positions[ticker]
|
||||
|
||||
def _log_trade(self, action: str, ticker: str, price: float, qty: float, side: int,
|
||||
pnl_abs: float = 0.0, pnl_pct: float = 0.0):
|
||||
pnl_abs: float = 0.0, pnl_pct: float = 0.0, reason: Optional[str] = None):
|
||||
ts = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
side_str = "LONG" if side == 1 else "SHORT"
|
||||
if action == "BUY":
|
||||
print(f"[{ts}] BUY {ticker} @ {price:.6f}, qty={qty:.6f}, side={side_str}, cash={self.cash:.2f}")
|
||||
elif action == "SELL":
|
||||
print(f"[{ts}] SELL {ticker} @ {price:.6f}, qty={qty:.6f}, "
|
||||
f"PnL={pnl_abs:+.2f} ({pnl_pct:+.2%}), "
|
||||
f"cumPnL={self.realized_pnl:+.2f}, cash={self.cash:.2f}")
|
||||
# zapis do logu w pamięci
|
||||
print(f"[{ts}] BUY {ticker} @ {price:.6f}, qty={qty:.6f}, side={'LONG' if side==1 else 'SHORT'}, cash={self.cash:.2f}")
|
||||
else:
|
||||
r = f", reason={reason}" if reason else ""
|
||||
print(f"[{ts}] SELL {ticker} @ {price:.6f}, qty={qty:.6f}, PnL={pnl_abs:+.2f} ({pnl_pct:+.2%}), cash={self.cash:.2f}{r}")
|
||||
self.trade_log.append({
|
||||
"time": ts,
|
||||
"action": action,
|
||||
"ticker": ticker,
|
||||
"price": float(price),
|
||||
"qty": float(qty),
|
||||
"side": int(side),
|
||||
"pnl_abs": float(pnl_abs),
|
||||
"pnl_pct": float(pnl_pct),
|
||||
"realized_pnl_cum": float(self.realized_pnl),
|
||||
"cash_after": float(self.cash)
|
||||
"time": ts, "action": action, "ticker": ticker, "price": float(price),
|
||||
"qty": float(qty), "side": int(side),
|
||||
"pnl_abs": float(pnl_abs), "pnl_pct": float(pnl_pct),
|
||||
"realized_pnl_cum": float(self.realized_pnl), "cash_after": float(self.cash),
|
||||
"reason": reason or ""
|
||||
})
|
||||
|
||||
# ---------- główna aktualizacja ----------
|
||||
def on_signals(self, sigs: List[dict]):
|
||||
"""
|
||||
sigs: lista dictów/obiektów z polami: ticker, price, signal, time
|
||||
BUY (1) / SELL (-1) / HOLD (0)
|
||||
"""
|
||||
clean = []
|
||||
# normalizacja
|
||||
clean: List[Dict[str, Any]] = []
|
||||
for s in sigs:
|
||||
if isinstance(s, dict):
|
||||
if s.get("error") is None and s.get("price") is not None and not math.isnan(s.get("price", float("nan"))):
|
||||
px = s.get("price")
|
||||
if s.get("error") is None and px is not None and not math.isnan(px):
|
||||
clean.append(s)
|
||||
else:
|
||||
if getattr(s, "error", None) is None and not math.isnan(getattr(s, "price", float("nan"))):
|
||||
clean.append({"ticker": s.ticker, "price": s.price, "signal": s.signal, "time": s.time})
|
||||
clean.append({
|
||||
"ticker": s.ticker, "price": s.price, "signal": s.signal, "time": s.time,
|
||||
"atr": getattr(s, "atr", None)
|
||||
})
|
||||
|
||||
# snapshot, gdy brak świeżych danych
|
||||
if not clean:
|
||||
vals = self._positions_values(None)
|
||||
prices_map = {t: self.last_prices.get(t, p["entry"]) for t, p in self.positions.items()}
|
||||
if ACCOUNTING_MODE == "stock":
|
||||
account_value = self.cash + vals["positions_net"]
|
||||
else:
|
||||
account_value = self.equity_from_start(prices_map)
|
||||
unreal = self.unrealized_pnl(prices_map)
|
||||
self.history.append({
|
||||
"time": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"equity": self.cash,
|
||||
"cash": self.cash,
|
||||
"open_positions": len(self.positions)
|
||||
"cash": float(self.cash),
|
||||
"positions_value": float(vals["positions_gross"]),
|
||||
"positions_net": float(vals["positions_net"]),
|
||||
"total_value": float(account_value),
|
||||
"equity_from_start": float(self.start_capital + self.realized_pnl + unreal),
|
||||
"unrealized_pnl": float(unreal),
|
||||
"realized_pnl_cum": float(self.realized_pnl),
|
||||
"open_positions": int(len(self.positions))
|
||||
})
|
||||
save_json("portfolio_history.json", self.history)
|
||||
return
|
||||
|
||||
prices = {s["ticker"]: s["price"] for s in clean}
|
||||
n = len(clean)
|
||||
per_trade_cash = max(self.cash / (n * 2), 0.0)
|
||||
# mapy cen
|
||||
prices = {s["ticker"]: float(s["price"]) for s in clean}
|
||||
self.last_prices.update(prices)
|
||||
|
||||
# 1) risk exits (SL/TP/TRAIL + BE)
|
||||
to_close = []
|
||||
for t, pos in list(self.positions.items()):
|
||||
price = prices.get(t)
|
||||
if price is None or math.isnan(price): continue
|
||||
self._update_trailing(t, pos, price)
|
||||
if pos["side"] == 1:
|
||||
if price <= pos["stop"]: to_close.append((t, "SL"))
|
||||
elif price >= pos["take"]: to_close.append((t, "TP"))
|
||||
elif price <= pos.get("trail_stop", -float("inf")): to_close.append((t, "TRAIL"))
|
||||
else:
|
||||
if price >= pos["stop"]: to_close.append((t, "SL"))
|
||||
elif price <= pos["take"]: to_close.append((t, "TP"))
|
||||
elif price >= pos.get("trail_stop", float("inf")): to_close.append((t, "TRAIL"))
|
||||
for t, reason in to_close:
|
||||
price = prices.get(t)
|
||||
if price is not None and not math.isnan(price) and t in self.positions:
|
||||
self._close_position(t, price, reason=reason)
|
||||
|
||||
# 2) zamknięcie na przeciwny/HOLD
|
||||
for s in clean:
|
||||
t, sig, price = s["ticker"], int(s["signal"]), float(s["price"])
|
||||
|
||||
# zamknięcie lub odwrócenie
|
||||
t, sig = s["ticker"], int(s["signal"])
|
||||
price = float(s["price"])
|
||||
if t in self.positions:
|
||||
pos = self.positions[t]
|
||||
if sig == 0 or sig != pos["side"]:
|
||||
qty = pos["qty"]
|
||||
entry = pos["entry"]
|
||||
side = pos["side"]
|
||||
self._close_position(t, price, reason="SIGNAL")
|
||||
|
||||
# 3) otwarcia – ATR sizing + limity
|
||||
candidates = [s for s in clean if s["ticker"] not in self.positions and int(s["signal"]) != 0]
|
||||
if MAX_NEW_POSITIONS_PER_CYCLE and MAX_NEW_POSITIONS_PER_CYCLE > 0:
|
||||
candidates = candidates[:MAX_NEW_POSITIONS_PER_CYCLE]
|
||||
|
||||
for s in candidates:
|
||||
t, sig, price = s["ticker"], int(s["signal"]), float(s["price"])
|
||||
atr = s.get("atr", None)
|
||||
if price <= 0: continue
|
||||
|
||||
stop, take, trail_best, trail_stop, one_r = self._init_risk(t, price, sig, atr)
|
||||
|
||||
# equity teraz (dla sizingu ryzyka)
|
||||
if ACCOUNTING_MODE == "margin":
|
||||
equity_now = self.start_capital + self.realized_pnl + self.unrealized_pnl(self.last_prices)
|
||||
else:
|
||||
vals_tmp = self._positions_values(self.last_prices)
|
||||
equity_now = self.cash + vals_tmp["positions_net"]
|
||||
|
||||
risk_amount = max(1e-6, equity_now * RISK_FRACTION)
|
||||
qty_risk = risk_amount / max(one_r, 1e-8)
|
||||
|
||||
# limit gotówki dla LONG
|
||||
per_trade_cash = max(MIN_TRADE_CASH, self.cash * ALLOC_FRACTION)
|
||||
qty_cash = per_trade_cash / max(price, 1e-12) if sig == 1 else float("inf")
|
||||
|
||||
qty = max(0.0, min(qty_risk, qty_cash))
|
||||
if qty <= 0: continue
|
||||
|
||||
# księgowanie gotówki przy otwarciu
|
||||
if sig == 1:
|
||||
cost = qty * price
|
||||
if self.cash < cost: continue
|
||||
self.cash -= cost
|
||||
else:
|
||||
if ACCOUNTING_MODE == "stock":
|
||||
self.cash += qty * price
|
||||
pnl_abs = (price - entry) * qty * side
|
||||
denom = max(qty * entry, 1e-12)
|
||||
pnl_pct = pnl_abs / denom
|
||||
self.realized_pnl += pnl_abs
|
||||
self._log_trade("SELL", t, price, qty, side, pnl_abs, pnl_pct)
|
||||
del self.positions[t]
|
||||
# margin: brak zmiany cash przy short open
|
||||
|
||||
# otwarcie
|
||||
if t not in self.positions and sig != 0 and per_trade_cash > 0:
|
||||
qty = per_trade_cash / price
|
||||
self.cash -= qty * price
|
||||
self.positions[t] = {"qty": qty, "entry": price, "side": sig}
|
||||
self._log_trade("BUY", t, price, qty, sig)
|
||||
self.positions[t] = {
|
||||
"qty": qty, "entry": price, "side": sig,
|
||||
"stop": stop, "take": take,
|
||||
"trail_best": trail_best, "trail_stop": trail_stop,
|
||||
"one_r": one_r, "be_done": False
|
||||
}
|
||||
self._log_trade("BUY" if sig == 1 else "SELL", t, price, qty, sig)
|
||||
|
||||
# 4) snapshot na bieżących/ostatnich cenach
|
||||
vals = self._positions_values(self.last_prices)
|
||||
prices_map = {t: self.last_prices.get(t, p["entry"]) for t, p in self.positions.items()}
|
||||
if ACCOUNTING_MODE == "stock":
|
||||
account_value = self.cash + vals["positions_net"]
|
||||
else:
|
||||
account_value = self.equity_from_start(prices_map)
|
||||
unreal = self.unrealized_pnl(prices_map)
|
||||
|
||||
equity = self.mark_to_market(prices)
|
||||
self.history.append({
|
||||
"time": clean[0]["time"],
|
||||
"equity": float(equity),
|
||||
"cash": float(self.cash),
|
||||
"open_positions": int(len(self.positions)),
|
||||
"realized_pnl_cum": float(self.realized_pnl)
|
||||
"positions_value": float(vals["positions_gross"]),
|
||||
"positions_net": float(vals["positions_net"]),
|
||||
"total_value": float(account_value),
|
||||
"equity_from_start": float(self.start_capital + self.realized_pnl + unreal),
|
||||
"unrealized_pnl": float(unreal),
|
||||
"realized_pnl_cum": float(self.realized_pnl),
|
||||
"open_positions": int(len(self.positions))
|
||||
})
|
||||
|
||||
# zapis plików JSON
|
||||
# 5) zapisy
|
||||
save_json("trade_log.json", self.trade_log)
|
||||
save_json("portfolio_history.json", self.history)
|
||||
save_json("positions.json", [{"ticker": t, **p} for t, p in self.positions.items()])
|
||||
|
||||
24
portfolio_history.json
Normal file
24
portfolio_history.json
Normal file
@ -0,0 +1,24 @@
|
||||
[
|
||||
{
|
||||
"time": "2025-08-15 10:29:00+00:00",
|
||||
"cash": 10000.0,
|
||||
"positions_value": 2999.9999999999804,
|
||||
"positions_net": -2999.9999999999804,
|
||||
"total_value": 10000.0,
|
||||
"equity_from_start": 10000.0,
|
||||
"unrealized_pnl": 0.0,
|
||||
"realized_pnl_cum": 0.0,
|
||||
"open_positions": 1
|
||||
},
|
||||
{
|
||||
"time": "2025-08-15 10:30:00+00:00",
|
||||
"cash": 9996.027808030158,
|
||||
"positions_value": 0.0,
|
||||
"positions_net": 0.0,
|
||||
"total_value": 9996.027808030158,
|
||||
"equity_from_start": 9996.027808030158,
|
||||
"unrealized_pnl": 0.0,
|
||||
"realized_pnl_cum": -3.972191969841845,
|
||||
"open_positions": 0
|
||||
}
|
||||
]
|
||||
1
positions.json
Normal file
1
positions.json
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
Binary file not shown.
@ -1,4 +0,0 @@
|
||||
yfinance
|
||||
pandas
|
||||
numpy
|
||||
Flask
|
||||
83
server.py
83
server.py
@ -1,83 +0,0 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from flask import Flask, jsonify, render_template, request
|
||||
from trader import TraderWorker
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
|
||||
)
|
||||
log = logging.getLogger("server")
|
||||
|
||||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
||||
worker = TraderWorker()
|
||||
|
||||
@app.before_request
|
||||
def _log_request():
|
||||
log.debug("REQ %s %s args=%s json=%s",
|
||||
request.method, request.path, dict(request.args), request.get_json(silent=True))
|
||||
|
||||
@app.after_request
|
||||
def _after(resp):
|
||||
resp.headers["Cache-Control"] = "no-store, max-age=0"
|
||||
resp.headers["Pragma"] = "no-cache"
|
||||
resp.headers["Expires"] = "0"
|
||||
log.debug("RESP %s %s %s", request.method, request.path, resp.status)
|
||||
return resp
|
||||
|
||||
@app.get("/")
|
||||
def index():
|
||||
return render_template("index.html")
|
||||
|
||||
@app.get("/api/status")
|
||||
def api_status():
|
||||
return jsonify(worker.status())
|
||||
|
||||
@app.get("/api/positions")
|
||||
def api_positions():
|
||||
return jsonify({"positions": worker.list_positions()})
|
||||
|
||||
@app.get("/api/trades")
|
||||
def api_trades():
|
||||
return jsonify({"trades": worker.list_trades()})
|
||||
|
||||
@app.get("/api/equity")
|
||||
def api_equity():
|
||||
return jsonify({"equity": worker.list_equity()})
|
||||
|
||||
@app.post("/api/start")
|
||||
def api_start():
|
||||
ok = worker.start()
|
||||
return jsonify({"started": ok, "running": worker.is_running()})
|
||||
|
||||
@app.post("/api/stop")
|
||||
def api_stop():
|
||||
ok = worker.stop()
|
||||
return jsonify({"stopped": ok, "running": worker.is_running()})
|
||||
|
||||
@app.post("/api/run-once")
|
||||
def api_run_once():
|
||||
took = worker.tick_once()
|
||||
return jsonify({"ok": True, "took_s": took})
|
||||
|
||||
# testowe (opcjonalne)
|
||||
@app.post("/api/test/long")
|
||||
def api_test_long():
|
||||
data = request.get_json(silent=True) or {}
|
||||
worker.test_open_long(data.get("ticker","AAPL"), data.get("price",123.45), data.get("size",1.0))
|
||||
return jsonify({"ok": True})
|
||||
|
||||
@app.post("/api/test/short")
|
||||
def api_test_short():
|
||||
data = request.get_json(silent=True) or {}
|
||||
worker.test_open_short(data.get("ticker","AAPL"), data.get("price",123.45), data.get("size",1.0))
|
||||
return jsonify({"ok": True})
|
||||
|
||||
@app.post("/api/test/close")
|
||||
def api_test_close():
|
||||
data = request.get_json(silent=True) or {}
|
||||
worker.test_close(data.get("ticker","AAPL"), data.get("price"))
|
||||
return jsonify({"ok": True})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000, debug=False)
|
||||
@ -1,71 +1,8 @@
|
||||
[
|
||||
{
|
||||
"ticker": "GBPUSD=X",
|
||||
"time": "2025-08-15 10:17:00+00:00",
|
||||
"price": 1.3561896085739136,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURUSD=X",
|
||||
"time": "2025-08-15 10:17:00+00:00",
|
||||
"price": 1.1691803932189941,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "CHFJPY=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 182.3179931640625,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "DOGE-USD",
|
||||
"time": "2025-08-15 10:15:00+00:00",
|
||||
"price": 0.2317201942205429,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "OP-USD",
|
||||
"time": "2025-08-15 10:11:00+00:00",
|
||||
"price": 0.7549265623092651,
|
||||
"signal": -1,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "BTC-USD",
|
||||
"time": "2025-08-15 10:15:00+00:00",
|
||||
"price": 118967.421875,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "NZDUSD=X",
|
||||
"time": "2025-08-15 10:16:00+00:00",
|
||||
"price": 0.5927330851554871,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURAUD=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 1.7958300113677979,
|
||||
"time": "2025-08-15 10:30:00+00:00",
|
||||
"price": 1.1689070463180542,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
@ -73,215 +10,8 @@
|
||||
},
|
||||
{
|
||||
"ticker": "USDCHF=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 0.8054800033569336,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "SOL-USD",
|
||||
"time": "2025-08-15 10:15:00+00:00",
|
||||
"price": 195.40365600585938,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "USDJPY=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 146.86399841308594,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "GBPJPY=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 199.14500427246094,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "XLM-USD",
|
||||
"time": "2025-08-15 10:16:00+00:00",
|
||||
"price": 0.42842021584510803,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURGBP=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 0.8619400262832642,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "LTC-USD",
|
||||
"time": "2025-08-15 10:14:00+00:00",
|
||||
"price": 121.15275573730469,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "USDCAD=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 1.3791099786758423,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "BNB-USD",
|
||||
"time": "2025-08-15 10:15:00+00:00",
|
||||
"price": 844.4680786132812,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "ETH-USD",
|
||||
"time": "2025-08-15 10:16:00+00:00",
|
||||
"price": 4635.28955078125,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "ETC-USD",
|
||||
"time": "2025-08-15 10:15:00+00:00",
|
||||
"price": 22.40416717529297,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "CADJPY=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 106.49600219726562,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "TRX-USD",
|
||||
"time": "2025-08-15 10:16:00+00:00",
|
||||
"price": 0.3588825762271881,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURJPY=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 171.65899658203125,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "TON-USD",
|
||||
"time": "2025-08-15 10:11:00+00:00",
|
||||
"price": 0.01708882860839367,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "ADA-USD",
|
||||
"time": "2025-08-15 10:16:00+00:00",
|
||||
"price": 0.951282799243927,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "NZDJPY=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 87.01699829101562,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "AUDJPY=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 95.58399963378906,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURCAD=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 1.611799955368042,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "BCH-USD",
|
||||
"time": "2025-08-15 10:15:00+00:00",
|
||||
"price": 596.3115844726562,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "DOT-USD",
|
||||
"time": "2025-08-15 10:15:00+00:00",
|
||||
"price": 4.005911827087402,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "ATOM-USD",
|
||||
"time": "2025-08-15 10:16:00+00:00",
|
||||
"price": 4.517627239227295,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "AUDNZD=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 1.0983599424362183,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "GC=F",
|
||||
"time": "2025-08-15 10:08:00+00:00",
|
||||
"price": 3386.10009765625,
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 0.8058599829673767,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
@ -289,44 +19,35 @@
|
||||
},
|
||||
{
|
||||
"ticker": "NEAR-USD",
|
||||
"time": "2025-08-15 10:11:00+00:00",
|
||||
"price": 2.7867467403411865,
|
||||
"signal": -1,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "GBPCAD=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 1.8698300123214722,
|
||||
"time": "2025-08-15 10:21:00+00:00",
|
||||
"price": 2.7824549674987793,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "XRP-USD",
|
||||
"time": "2025-08-15 10:16:00+00:00",
|
||||
"price": 3.111118793487549,
|
||||
"ticker": "USDJPY=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 146.8820037841797,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURCHF=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 0.9409899711608887,
|
||||
"ticker": "ETH-USD",
|
||||
"time": "2025-08-15 10:28:00+00:00",
|
||||
"price": 4645.15771484375,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "LINK-USD",
|
||||
"time": "2025-08-15 10:16:00+00:00",
|
||||
"price": 22.371389389038086,
|
||||
"ticker": "CADJPY=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 106.47799682617188,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
@ -334,8 +55,287 @@
|
||||
},
|
||||
{
|
||||
"ticker": "AUDUSD=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 0.6509992480278015,
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 0.6507663130760193,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "OP-USD",
|
||||
"time": "2025-08-15 10:26:00+00:00",
|
||||
"price": 0.7553843259811401,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURJPY=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 171.62600708007812,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "NZDUSD=X",
|
||||
"time": "2025-08-15 10:30:00+00:00",
|
||||
"price": 0.5924872159957886,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "ETC-USD",
|
||||
"time": "2025-08-15 10:30:00+00:00",
|
||||
"price": 22.407447814941406,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "GC=F",
|
||||
"time": "2025-08-15 10:21:00+00:00",
|
||||
"price": 3386.89990234375,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "GBPJPY=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 199.0590057373047,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "AUDJPY=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 95.55599975585938,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "TRX-USD",
|
||||
"time": "2025-08-15 10:29:00+00:00",
|
||||
"price": 0.35983800888061523,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "TON-USD",
|
||||
"time": "2025-08-15 10:26:00+00:00",
|
||||
"price": 0.01708882860839367,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "LTC-USD",
|
||||
"time": "2025-08-15 10:29:00+00:00",
|
||||
"price": 121.18510437011719,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURCAD=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 1.6117500066757202,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "AUDNZD=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 1.0984100103378296,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "USDCAD=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 1.3794000148773193,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "ATOM-USD",
|
||||
"time": "2025-08-15 10:29:00+00:00",
|
||||
"price": 4.5298662185668945,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "BCH-USD",
|
||||
"time": "2025-08-15 10:30:00+00:00",
|
||||
"price": 595.4796142578125,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "LINK-USD",
|
||||
"time": "2025-08-15 10:29:00+00:00",
|
||||
"price": 22.34357452392578,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "XRP-USD",
|
||||
"time": "2025-08-15 10:29:00+00:00",
|
||||
"price": 3.117891550064087,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "DOT-USD",
|
||||
"time": "2025-08-15 10:30:00+00:00",
|
||||
"price": 4.020590305328369,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "BTC-USD",
|
||||
"time": "2025-08-15 10:29:00+00:00",
|
||||
"price": 119062.5703125,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "GBPUSD=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 1.3552889823913574,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "CHFJPY=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 182.2550048828125,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "ADA-USD",
|
||||
"time": "2025-08-15 10:30:00+00:00",
|
||||
"price": 0.9589924216270447,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "XLM-USD",
|
||||
"time": "2025-08-15 10:29:00+00:00",
|
||||
"price": 0.42904016375541687,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURCHF=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 0.9415299892425537,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "BNB-USD",
|
||||
"time": "2025-08-15 10:30:00+00:00",
|
||||
"price": 845.3945922851562,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "DOGE-USD",
|
||||
"time": "2025-08-15 10:30:00+00:00",
|
||||
"price": 0.23236910998821259,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "NZDJPY=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 86.98799896240234,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURGBP=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 0.8621399998664856,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "SOL-USD",
|
||||
"time": "2025-08-15 10:29:00+00:00",
|
||||
"price": 195.72109985351562,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURAUD=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 1.7960000038146973,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "GBPCAD=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 1.869420051574707,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
@ -1,110 +1,22 @@
|
||||
{
|
||||
"time": "2025-08-15 12:19:02",
|
||||
"time": "2025-08-15 12:32:18",
|
||||
"last_history": {
|
||||
"time": "2025-08-15 10:17:00+00:00",
|
||||
"cash": 10000.0,
|
||||
"positions_value": 5999.999999999975,
|
||||
"positions_net": -5999.999999999975,
|
||||
"total_value": 10000.0,
|
||||
"equity_from_start": 10000.0,
|
||||
"time": "2025-08-15 10:30:00+00:00",
|
||||
"cash": 9996.027808030158,
|
||||
"positions_value": 0.0,
|
||||
"positions_net": 0.0,
|
||||
"total_value": 9996.027808030158,
|
||||
"equity_from_start": 9996.027808030158,
|
||||
"unrealized_pnl": 0.0,
|
||||
"realized_pnl_cum": 0.0,
|
||||
"open_positions": 2
|
||||
"realized_pnl_cum": -3.972191969841845,
|
||||
"open_positions": 0
|
||||
},
|
||||
"positions": [
|
||||
{
|
||||
"ticker": "OP-USD",
|
||||
"qty": 3973.896468582607,
|
||||
"entry": 0.7549265623092651,
|
||||
"side": -1,
|
||||
"stop": 0.7624758279323578,
|
||||
"take": 0.7398280310630798,
|
||||
"trail_best": 0.7549265623092651,
|
||||
"trail_stop": 0.7624758279323578,
|
||||
"one_r": 0.00754926562309266,
|
||||
"be_done": false
|
||||
},
|
||||
{
|
||||
"ticker": "NEAR-USD",
|
||||
"qty": 1076.5240904642392,
|
||||
"entry": 2.7867467403411865,
|
||||
"side": -1,
|
||||
"stop": 2.8146142077445986,
|
||||
"take": 2.731011805534363,
|
||||
"trail_best": 2.7867467403411865,
|
||||
"trail_stop": 2.8146142077445986,
|
||||
"one_r": 0.02786746740341206,
|
||||
"be_done": false
|
||||
}
|
||||
],
|
||||
"positions": [],
|
||||
"signals": [
|
||||
{
|
||||
"ticker": "GBPUSD=X",
|
||||
"time": "2025-08-15 10:17:00+00:00",
|
||||
"price": 1.3561896085739136,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURUSD=X",
|
||||
"time": "2025-08-15 10:17:00+00:00",
|
||||
"price": 1.1691803932189941,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "CHFJPY=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 182.3179931640625,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "DOGE-USD",
|
||||
"time": "2025-08-15 10:15:00+00:00",
|
||||
"price": 0.2317201942205429,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "OP-USD",
|
||||
"time": "2025-08-15 10:11:00+00:00",
|
||||
"price": 0.7549265623092651,
|
||||
"signal": -1,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "BTC-USD",
|
||||
"time": "2025-08-15 10:15:00+00:00",
|
||||
"price": 118967.421875,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "NZDUSD=X",
|
||||
"time": "2025-08-15 10:16:00+00:00",
|
||||
"price": 0.5927330851554871,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURAUD=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 1.7958300113677979,
|
||||
"time": "2025-08-15 10:30:00+00:00",
|
||||
"price": 1.1689070463180542,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
@ -112,215 +24,8 @@
|
||||
},
|
||||
{
|
||||
"ticker": "USDCHF=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 0.8054800033569336,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "SOL-USD",
|
||||
"time": "2025-08-15 10:15:00+00:00",
|
||||
"price": 195.40365600585938,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "USDJPY=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 146.86399841308594,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "GBPJPY=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 199.14500427246094,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "XLM-USD",
|
||||
"time": "2025-08-15 10:16:00+00:00",
|
||||
"price": 0.42842021584510803,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURGBP=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 0.8619400262832642,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "LTC-USD",
|
||||
"time": "2025-08-15 10:14:00+00:00",
|
||||
"price": 121.15275573730469,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "USDCAD=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 1.3791099786758423,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "BNB-USD",
|
||||
"time": "2025-08-15 10:15:00+00:00",
|
||||
"price": 844.4680786132812,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "ETH-USD",
|
||||
"time": "2025-08-15 10:16:00+00:00",
|
||||
"price": 4635.28955078125,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "ETC-USD",
|
||||
"time": "2025-08-15 10:15:00+00:00",
|
||||
"price": 22.40416717529297,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "CADJPY=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 106.49600219726562,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "TRX-USD",
|
||||
"time": "2025-08-15 10:16:00+00:00",
|
||||
"price": 0.3588825762271881,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURJPY=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 171.65899658203125,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "TON-USD",
|
||||
"time": "2025-08-15 10:11:00+00:00",
|
||||
"price": 0.01708882860839367,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "ADA-USD",
|
||||
"time": "2025-08-15 10:16:00+00:00",
|
||||
"price": 0.951282799243927,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "NZDJPY=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 87.01699829101562,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "AUDJPY=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 95.58399963378906,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURCAD=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 1.611799955368042,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "BCH-USD",
|
||||
"time": "2025-08-15 10:15:00+00:00",
|
||||
"price": 596.3115844726562,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "DOT-USD",
|
||||
"time": "2025-08-15 10:15:00+00:00",
|
||||
"price": 4.005911827087402,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "ATOM-USD",
|
||||
"time": "2025-08-15 10:16:00+00:00",
|
||||
"price": 4.517627239227295,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "AUDNZD=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 1.0983599424362183,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "GC=F",
|
||||
"time": "2025-08-15 10:08:00+00:00",
|
||||
"price": 3386.10009765625,
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 0.8058599829673767,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
@ -328,44 +33,35 @@
|
||||
},
|
||||
{
|
||||
"ticker": "NEAR-USD",
|
||||
"time": "2025-08-15 10:11:00+00:00",
|
||||
"price": 2.7867467403411865,
|
||||
"signal": -1,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "GBPCAD=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 1.8698300123214722,
|
||||
"time": "2025-08-15 10:21:00+00:00",
|
||||
"price": 2.7824549674987793,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "XRP-USD",
|
||||
"time": "2025-08-15 10:16:00+00:00",
|
||||
"price": 3.111118793487549,
|
||||
"ticker": "USDJPY=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 146.8820037841797,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURCHF=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 0.9409899711608887,
|
||||
"ticker": "ETH-USD",
|
||||
"time": "2025-08-15 10:28:00+00:00",
|
||||
"price": 4645.15771484375,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "LINK-USD",
|
||||
"time": "2025-08-15 10:16:00+00:00",
|
||||
"price": 22.371389389038086,
|
||||
"ticker": "CADJPY=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 106.47799682617188,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
@ -373,8 +69,287 @@
|
||||
},
|
||||
{
|
||||
"ticker": "AUDUSD=X",
|
||||
"time": "2025-08-15 10:18:00+00:00",
|
||||
"price": 0.6509992480278015,
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 0.6507663130760193,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "OP-USD",
|
||||
"time": "2025-08-15 10:26:00+00:00",
|
||||
"price": 0.7553843259811401,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURJPY=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 171.62600708007812,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "NZDUSD=X",
|
||||
"time": "2025-08-15 10:30:00+00:00",
|
||||
"price": 0.5924872159957886,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "ETC-USD",
|
||||
"time": "2025-08-15 10:30:00+00:00",
|
||||
"price": 22.407447814941406,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "GC=F",
|
||||
"time": "2025-08-15 10:21:00+00:00",
|
||||
"price": 3386.89990234375,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "GBPJPY=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 199.0590057373047,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "AUDJPY=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 95.55599975585938,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "TRX-USD",
|
||||
"time": "2025-08-15 10:29:00+00:00",
|
||||
"price": 0.35983800888061523,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "TON-USD",
|
||||
"time": "2025-08-15 10:26:00+00:00",
|
||||
"price": 0.01708882860839367,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "LTC-USD",
|
||||
"time": "2025-08-15 10:29:00+00:00",
|
||||
"price": 121.18510437011719,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURCAD=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 1.6117500066757202,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "AUDNZD=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 1.0984100103378296,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "USDCAD=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 1.3794000148773193,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "ATOM-USD",
|
||||
"time": "2025-08-15 10:29:00+00:00",
|
||||
"price": 4.5298662185668945,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "BCH-USD",
|
||||
"time": "2025-08-15 10:30:00+00:00",
|
||||
"price": 595.4796142578125,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "LINK-USD",
|
||||
"time": "2025-08-15 10:29:00+00:00",
|
||||
"price": 22.34357452392578,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "XRP-USD",
|
||||
"time": "2025-08-15 10:29:00+00:00",
|
||||
"price": 3.117891550064087,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "DOT-USD",
|
||||
"time": "2025-08-15 10:30:00+00:00",
|
||||
"price": 4.020590305328369,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "BTC-USD",
|
||||
"time": "2025-08-15 10:29:00+00:00",
|
||||
"price": 119062.5703125,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "GBPUSD=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 1.3552889823913574,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "CHFJPY=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 182.2550048828125,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "ADA-USD",
|
||||
"time": "2025-08-15 10:30:00+00:00",
|
||||
"price": 0.9589924216270447,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "XLM-USD",
|
||||
"time": "2025-08-15 10:29:00+00:00",
|
||||
"price": 0.42904016375541687,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURCHF=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 0.9415299892425537,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "BNB-USD",
|
||||
"time": "2025-08-15 10:30:00+00:00",
|
||||
"price": 845.3945922851562,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "DOGE-USD",
|
||||
"time": "2025-08-15 10:30:00+00:00",
|
||||
"price": 0.23236910998821259,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "NZDJPY=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 86.98799896240234,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURGBP=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 0.8621399998664856,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "SOL-USD",
|
||||
"time": "2025-08-15 10:29:00+00:00",
|
||||
"price": 195.72109985351562,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "EURAUD=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 1.7960000038146973,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"ticker": "GBPCAD=X",
|
||||
"time": "2025-08-15 10:31:00+00:00",
|
||||
"price": 1.869420051574707,
|
||||
"signal": 0,
|
||||
"period": "7d",
|
||||
"interval": "1m",
|
||||
414
static/app.js
414
static/app.js
@ -1,253 +1,221 @@
|
||||
// app.js
|
||||
// static/app.js
|
||||
const fmt2 = (x) => (x == null || isNaN(x) ? "—" : Number(x).toFixed(2));
|
||||
const fmt6 = (x) => (x == null || isNaN(x) ? "—" : Number(x).toFixed(6));
|
||||
const fmtPct= (x) => (x == null || isNaN(x) ? "—" : (Number(x) * 100).toFixed(2) + "%");
|
||||
|
||||
// ── utils ──────────────────────────────────────────────────────────────────────
|
||||
const fmtNum = (x, d = 2) =>
|
||||
x === null || x === undefined || Number.isNaN(x) ? "–" : Number(x).toFixed(d);
|
||||
|
||||
// Potencjalne adresy backendu (API). Pierwszy to aktualny origin UI.
|
||||
const apiCandidates = [
|
||||
window.location.origin,
|
||||
"http://127.0.0.1:8000",
|
||||
"http://localhost:8000",
|
||||
"http://172.27.20.120:8000", // z logów serwera
|
||||
];
|
||||
|
||||
let API_BASE = null;
|
||||
let warnedMixed = false;
|
||||
|
||||
function withTimeout(ms = 6000) {
|
||||
const ctrl = new AbortController();
|
||||
const id = setTimeout(() => ctrl.abort("timeout"), ms);
|
||||
return { signal: ctrl.signal, done: () => clearTimeout(id) };
|
||||
async function getJSON(url) {
|
||||
const r = await fetch(url, { cache: "no-store" });
|
||||
if (!r.ok) throw new Error(`${url}: ${r.status}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
function makeUrl(path) {
|
||||
return `${API_BASE}${path}${path.includes("?") ? "&" : "?"}_ts=${Date.now()}`;
|
||||
}
|
||||
// prosty wykres linii na canvas (bez bibliotek)
|
||||
function drawLineChart(canvas, points) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
const pad = 32;
|
||||
const w = canvas.width, h = canvas.height;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
function setBadge(id, text, ok = true) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.textContent = text;
|
||||
el.className = ok ? "badge ok" : "badge err";
|
||||
}
|
||||
|
||||
function setBadgeTitle(id, title) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.title = title || "";
|
||||
}
|
||||
|
||||
function warnMixedContent(base) {
|
||||
if (!warnedMixed && location.protocol === "https:" && base?.startsWith("http://")) {
|
||||
warnedMixed = true;
|
||||
console.warn(
|
||||
"[api] UI działa przez HTTPS, a API przez HTTP — przeglądarka może blokować żądania (mixed content)."
|
||||
);
|
||||
alert(
|
||||
"UI działa przez HTTPS, a API przez HTTP. Uruchom UI przez HTTP albo włącz HTTPS dla API — inaczej przeglądarka zablokuje żądania."
|
||||
);
|
||||
if (!points || points.length === 0) {
|
||||
ctx.fillStyle = "#9aa3b2";
|
||||
ctx.fillText("Brak danych", 10, 20);
|
||||
return;
|
||||
}
|
||||
|
||||
const n = points.length;
|
||||
const ys = points.map(p => p.y);
|
||||
const minY = Math.min(...ys), maxY = Math.max(...ys);
|
||||
const yLo = minY === maxY ? minY - 1 : minY;
|
||||
const yHi = minY === maxY ? maxY + 1 : maxY;
|
||||
|
||||
const x0 = pad, y0 = h - pad, x1 = w - pad, y1 = pad;
|
||||
const xScale = (i) => x0 + (i / (n - 1)) * (x1 - x0);
|
||||
const yScale = (y) => y0 - ((y - yLo) / (yHi - yLo)) * (y0 - y1);
|
||||
|
||||
// osie
|
||||
ctx.strokeStyle = "#242a36";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x0, y0); ctx.lineTo(x1, y0);
|
||||
ctx.moveTo(x0, y0); ctx.lineTo(x0, y1);
|
||||
ctx.stroke();
|
||||
|
||||
// siatka
|
||||
ctx.strokeStyle = "#1b2130";
|
||||
[0.25, 0.5, 0.75].forEach(f => {
|
||||
const yy = y0 - (y0 - y1) * f;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x0, yy); ctx.lineTo(x1, yy);
|
||||
ctx.stroke();
|
||||
});
|
||||
|
||||
// podpisy min/max
|
||||
ctx.fillStyle = "#9aa3b2";
|
||||
ctx.font = "12px system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial";
|
||||
ctx.fillText(fmt2(yHi), 6, y1 + 10);
|
||||
ctx.fillText(fmt2(yLo), 6, y0 - 2);
|
||||
|
||||
// linia
|
||||
ctx.strokeStyle = "#4da3ff";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(xScale(0), yScale(ys[0]));
|
||||
for (let i = 1; i < n; i++) ctx.lineTo(xScale(i), yScale(ys[i]));
|
||||
ctx.stroke();
|
||||
|
||||
// kropka na końcu
|
||||
const lastX = xScale(n - 1), lastY = yScale(ys[n - 1]);
|
||||
ctx.fillStyle = "#e7e9ee";
|
||||
ctx.beginPath(); ctx.arc(lastX, lastY, 3, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.fillText(fmt2(ys[n - 1]), lastX + 6, lastY - 6);
|
||||
}
|
||||
|
||||
// ── autodetekcja backendu ─────────────────────────────────────────────────────
|
||||
async function pickBackend() {
|
||||
for (const base of apiCandidates) {
|
||||
try {
|
||||
const t = withTimeout(2500);
|
||||
const r = await fetch(`${base}/api/status?_ts=${Date.now()}`, {
|
||||
cache: "no-store",
|
||||
signal: t.signal,
|
||||
});
|
||||
t.done();
|
||||
if (r.ok) {
|
||||
API_BASE = base;
|
||||
console.debug("[api] using", API_BASE);
|
||||
warnMixedContent(API_BASE);
|
||||
setBadgeTitle("loopState", `API: ${API_BASE}`);
|
||||
return;
|
||||
}
|
||||
console.debug("[api] probe", base, "->", r.status);
|
||||
} catch (e) {
|
||||
// ignorujemy i próbujemy kolejny kandydat
|
||||
console.debug("[api] probe fail", base, e?.message || e);
|
||||
}
|
||||
}
|
||||
throw new Error("Nie znaleziono działającego backendu (API_BASE). Upewnij się, że server.py działa na porcie 8000.");
|
||||
}
|
||||
|
||||
// ── API helpers ───────────────────────────────────────────────────────────────
|
||||
async function apiGet(path) {
|
||||
const url = makeUrl(path);
|
||||
const t0 = performance.now();
|
||||
const t = withTimeout(6000);
|
||||
async function loadAll() {
|
||||
try {
|
||||
const r = await fetch(url, { cache: "no-store", signal: t.signal });
|
||||
const t1 = performance.now();
|
||||
console.debug("[api] GET", url, r.status, (t1 - t0).toFixed(1) + "ms");
|
||||
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
|
||||
return r.json();
|
||||
} finally {
|
||||
t.done();
|
||||
}
|
||||
}
|
||||
const [snap, hist, pos, trades] = await Promise.all([
|
||||
getJSON("/api/snapshot"),
|
||||
getJSON("/api/history"),
|
||||
getJSON("/api/positions"),
|
||||
getJSON("/api/trades"),
|
||||
]);
|
||||
|
||||
async function apiPost(path, body) {
|
||||
const url = makeUrl(path);
|
||||
const t0 = performance.now();
|
||||
const t = withTimeout(6000);
|
||||
try {
|
||||
const r = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: body ? { "Content-Type": "application/json" } : undefined,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
signal: t.signal,
|
||||
// czas
|
||||
document.getElementById("last-update").textContent =
|
||||
"Ostatnia aktualizacja: " + (snap?.last_history?.time || "—");
|
||||
|
||||
// ===== ostatni wiersz historii =====
|
||||
const last = Array.isArray(hist) && hist.length ? hist[hist.length - 1] : null;
|
||||
|
||||
const cashVal = Number(last?.cash ?? 0);
|
||||
const totalVal = Number(
|
||||
last?.total_value ??
|
||||
(Number(last?.cash ?? 0) + Number(last?.positions_net ?? 0)) // fallback dla starszych logów
|
||||
);
|
||||
|
||||
// KARTY
|
||||
document.getElementById("cash").textContent = fmt2(cashVal);
|
||||
document.getElementById("total-value").textContent = fmt2(totalVal);
|
||||
|
||||
// mapa ostatnich cen do liczenia zysku (unrealized)
|
||||
const lastPrice = new Map();
|
||||
(snap?.signals || []).forEach(s => {
|
||||
const px = Number(s.price);
|
||||
if (!isNaN(px)) lastPrice.set(s.ticker, px);
|
||||
});
|
||||
const t1 = performance.now();
|
||||
console.debug("[api] POST", url, r.status, (t1 - t0).toFixed(1) + "ms");
|
||||
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
|
||||
return r.json();
|
||||
} finally {
|
||||
t.done();
|
||||
}
|
||||
}
|
||||
|
||||
// ── refreshers ────────────────────────────────────────────────────────────────
|
||||
async function refreshStatus() {
|
||||
try {
|
||||
const s = await apiGet("/api/status");
|
||||
setBadge("loopState", s.running ? "RUNNING" : "STOPPED", s.running);
|
||||
setBadgeTitle("loopState", `API: ${API_BASE} | last_action=${s.last_action || "–"}`);
|
||||
// Zysk = suma niezrealizowanych PnL na otwartych pozycjach
|
||||
let unrealPnL = 0;
|
||||
(pos || []).forEach(p => {
|
||||
const price = Number(lastPrice.get(p.ticker));
|
||||
const entry = Number(p.entry);
|
||||
const qty = Number(p.qty);
|
||||
const side = Number(p.side);
|
||||
if (isNaN(price) || isNaN(entry) || isNaN(qty) || isNaN(side)) return;
|
||||
unrealPnL += side === 1 ? (price - entry) * qty : (entry - price) * qty;
|
||||
});
|
||||
const unrlEl = document.getElementById("unrealized");
|
||||
unrlEl.textContent = fmt2(unrealPnL);
|
||||
unrlEl.classList.remove("pnl-positive", "pnl-negative");
|
||||
if (unrealPnL > 0) unrlEl.classList.add("pnl-positive");
|
||||
else if (unrealPnL < 0) unrlEl.classList.add("pnl-negative");
|
||||
|
||||
const roundEl = document.getElementById("roundNo");
|
||||
if (roundEl) roundEl.textContent = s.round ?? "–";
|
||||
// liczba otwartych pozycji
|
||||
document.getElementById("open-pos").textContent =
|
||||
Number(last?.open_positions ?? (pos?.length ?? 0));
|
||||
|
||||
const cashEl = document.getElementById("cash");
|
||||
if (cashEl) cashEl.textContent = fmtNum(s.cash, 2);
|
||||
// ===== WYKRES WARTOŚCI KONTA (TOTAL) =====
|
||||
const totalCanvas = document.getElementById("totalChart");
|
||||
const totalPoints = (hist || [])
|
||||
.map((row, i) => {
|
||||
const v = (row.total_value != null)
|
||||
? Number(row.total_value)
|
||||
: Number(row.cash ?? 0) + Number(row.positions_net ?? 0);
|
||||
return { x: i, y: v };
|
||||
})
|
||||
.filter(p => !isNaN(p.y))
|
||||
.slice(-500);
|
||||
drawLineChart(totalCanvas, totalPoints);
|
||||
|
||||
// now-playing
|
||||
const stageEl = document.getElementById("stage");
|
||||
if (stageEl) stageEl.textContent = (s.stage || "idle").toUpperCase();
|
||||
// ===== WYKRES GOTÓWKI (CASH) =====
|
||||
const cashCanvas = document.getElementById("cashChart");
|
||||
const cashPoints = (hist || [])
|
||||
.map((row, i) => ({ x: i, y: Number(row.cash ?? NaN) }))
|
||||
.filter(p => !isNaN(p.y))
|
||||
.slice(-500);
|
||||
drawLineChart(cashCanvas, cashPoints);
|
||||
|
||||
const tickerEl = document.getElementById("ticker");
|
||||
if (tickerEl) tickerEl.textContent = s.current_ticker || "–";
|
||||
// ===== POZYCJE =====
|
||||
const posBody = document.querySelector("#positions-table tbody");
|
||||
posBody.innerHTML = "";
|
||||
(pos || []).forEach((p) => {
|
||||
const price = Number(lastPrice.get(p.ticker));
|
||||
const entry = Number(p.entry);
|
||||
const qty = Number(p.qty);
|
||||
const side = Number(p.side);
|
||||
|
||||
const idx = s.current_index ?? 0;
|
||||
const total = s.tickers_total ?? 0;
|
||||
let upnl = NaN, upct = NaN;
|
||||
if (!isNaN(price) && !isNaN(entry) && !isNaN(qty) && !isNaN(side)) {
|
||||
upnl = side === 1 ? (price - entry) * qty : (entry - price) * qty;
|
||||
const denom = Math.max(qty * entry, 1e-12);
|
||||
upct = upnl / denom;
|
||||
}
|
||||
const pnlClass = upnl > 0 ? "pnl-positive" : (upnl < 0 ? "pnl-negative" : "");
|
||||
|
||||
const progressTextEl = document.getElementById("progressText");
|
||||
if (progressTextEl) progressTextEl.textContent = `${Math.min(idx + 1, total)} / ${total}`;
|
||||
|
||||
const prog = document.getElementById("progress");
|
||||
if (prog) {
|
||||
prog.max = total || 1;
|
||||
prog.value = Math.min(idx + 1, total) || 0;
|
||||
}
|
||||
|
||||
const lastActionEl = document.getElementById("lastAction");
|
||||
if (lastActionEl) lastActionEl.textContent = s.last_action || "–";
|
||||
} catch (e) {
|
||||
console.error("status error:", e);
|
||||
setBadge("loopState", "ERR", false);
|
||||
setBadgeTitle("loopState", `API: ${API_BASE || "—"} | ${e?.message || e}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPositions() {
|
||||
try {
|
||||
const { positions } = await apiGet("/api/positions");
|
||||
const tbody = document.querySelector("#positions tbody");
|
||||
if (!tbody) return;
|
||||
tbody.innerHTML = "";
|
||||
for (const p of positions) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<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>
|
||||
<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>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("positions error:", e);
|
||||
}
|
||||
}
|
||||
posBody.appendChild(tr);
|
||||
});
|
||||
|
||||
async function refreshTrades() {
|
||||
try {
|
||||
const { trades } = await apiGet("/api/trades");
|
||||
const tbody = document.querySelector("#trades tbody");
|
||||
if (!tbody) return;
|
||||
tbody.innerHTML = "";
|
||||
trades.slice(-50).reverse().forEach((t) => {
|
||||
// ===== SYGNAŁY =====
|
||||
const sigBody = document.querySelector("#signals-table tbody");
|
||||
sigBody.innerHTML = "";
|
||||
const signals = (snap?.signals || []).slice().sort((a,b)=>a.ticker.localeCompare(b.ticker));
|
||||
signals.forEach((s) => {
|
||||
const sigTxt = s.signal === 1 ? "BUY" : (s.signal === -1 ? "SELL" : "HOLD");
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<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>
|
||||
<td>${s.ticker}</td>
|
||||
<td>${s.time}</td>
|
||||
<td>${fmt6(s.price)}</td>
|
||||
<td class="${sigTxt}">${sigTxt}</td>
|
||||
<td>${s.interval}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
sigBody.appendChild(tr);
|
||||
});
|
||||
|
||||
// ===== TRANSAKCJE =====
|
||||
const tradesBody = document.querySelector("#trades-table tbody");
|
||||
tradesBody.innerHTML = "";
|
||||
(trades || []).slice(-50).reverse().forEach((t) => {
|
||||
const pnlClass = t.pnl_abs > 0 ? "pnl-positive" : (t.pnl_abs < 0 ? "pnl-negative" : "");
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<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("trades error:", e);
|
||||
console.error("loadAll error:", e);
|
||||
document.getElementById("last-update").textContent = "Błąd ładowania danych";
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAll() {
|
||||
await Promise.all([refreshStatus(), refreshPositions(), refreshTrades()]);
|
||||
}
|
||||
|
||||
// ── auto refresh ──────────────────────────────────────────────────────────────
|
||||
let timer = null;
|
||||
let currentInterval = 2000; // domyślnie 2s (zgodne z Twoim selectem)
|
||||
|
||||
function startAutoRefresh() {
|
||||
if (timer) return;
|
||||
timer = setInterval(refreshAll, currentInterval);
|
||||
console.debug("[ui] auto refresh started", currentInterval, "ms");
|
||||
}
|
||||
function stopAutoRefresh() {
|
||||
if (!timer) return;
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
console.debug("[ui] auto refresh stopped");
|
||||
}
|
||||
|
||||
// ── bootstrap ────────────────────────────────────────────────────────────────
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Przyciski
|
||||
document.getElementById("btnStart")?.addEventListener("click", async () => {
|
||||
try { await apiPost("/api/start"); await refreshStatus(); } catch (e) { console.error(e); }
|
||||
});
|
||||
document.getElementById("btnStop")?.addEventListener("click", async () => {
|
||||
try { await apiPost("/api/stop"); await refreshStatus(); } catch (e) { console.error(e); }
|
||||
});
|
||||
document.getElementById("btnTick")?.addEventListener("click", async () => {
|
||||
try { await apiPost("/api/run-once"); await refreshAll(); } catch (e) { console.error(e); }
|
||||
});
|
||||
|
||||
const sel = document.getElementById("refreshMs");
|
||||
sel?.addEventListener("change", () => {
|
||||
currentInterval = parseInt(sel.value, 10);
|
||||
if (timer) { stopAutoRefresh(); startAutoRefresh(); }
|
||||
});
|
||||
|
||||
document.getElementById("autoOn")?.addEventListener("click", startAutoRefresh);
|
||||
document.getElementById("autoOff")?.addEventListener("click", stopAutoRefresh);
|
||||
|
||||
// Autodetekcja backendu przed pierwszym odświeżeniem
|
||||
try {
|
||||
await pickBackend();
|
||||
await refreshAll();
|
||||
startAutoRefresh();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setBadge("loopState", "NO API", false);
|
||||
setBadgeTitle("loopState", e?.message || String(e));
|
||||
alert("UI nie może połączyć się z backendem (port 8000). Uruchom server.py lub zaktualizuj API_BASE w app.js.");
|
||||
}
|
||||
});
|
||||
document.getElementById("refresh-btn").addEventListener("click", loadAll);
|
||||
loadAll();
|
||||
setInterval(loadAll, 1000);
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
:root{
|
||||
--border:#e5e7eb;
|
||||
--muted:#64748b;
|
||||
}
|
||||
*{ box-sizing:border-box; }
|
||||
body{ font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; margin:0; color:#0f172a; background:#fff; }
|
||||
.container{ max-width: 1100px; margin: 0 auto; padding: 16px; }
|
||||
header{ display:flex; align-items:center; justify-content:space-between; margin-bottom: 16px; gap: 12px; }
|
||||
h1{ margin:0; font-size: 22px; }
|
||||
h2{ font-size:18px; margin: 18px 0 10px; }
|
||||
h3{ font-size:16px; margin:0; }
|
||||
|
||||
#statusBar{ display:flex; align-items:center; gap: 12px; flex-wrap: wrap; }
|
||||
#statusBar span{ font-size:14px; color:#0f172a; }
|
||||
.btn{
|
||||
appearance:none; border:1px solid var(--border); background:#0f172a; color:#fff;
|
||||
padding:8px 12px; border-radius:10px; cursor:pointer; font-weight:600;
|
||||
}
|
||||
.btn:hover{ opacity:.9; }
|
||||
.btn-secondary{ background:#475569; }
|
||||
|
||||
.grid{ display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin: 12px 0 24px; }
|
||||
.card{ background:#0f172a0d; border:1px solid var(--border); border-radius:12px; padding:12px; }
|
||||
.kpi{ font-size: 24px; font-weight: 700; margin-top: 8px; }
|
||||
.table-wrap{ overflow:auto; border:1px solid var(--border); border-radius:12px; }
|
||||
table{ width:100%; border-collapse: collapse; font-size: 14px; }
|
||||
th, td{ padding: 8px 10px; border-bottom: 1px solid #f1f5f9; white-space: nowrap; }
|
||||
thead th{ position: sticky; top: 0; background: #fff; z-index: 1; }
|
||||
tbody tr:hover{ background:#f8fafc; }
|
||||
|
||||
.pnl-pos{ color: #166534; font-weight:600; }
|
||||
.pnl-neg{ color: #991b1b; font-weight:600; }
|
||||
|
||||
.badge{
|
||||
padding:3px 8px; border-radius:999px; font-size:12px; border:1px solid var(--border);
|
||||
display:inline-block;
|
||||
}
|
||||
.badge.long{ background:#ecfdf5; border-color:#bbf7d0; }
|
||||
.badge.short{ background:#fef2f2; border-color:#fecaca; }
|
||||
|
||||
.empty{ text-align:center; color: var(--muted); padding: 14px; }
|
||||
93
strategy.py
93
strategy.py
@ -1,93 +0,0 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from config import CFG
|
||||
# zakładam, że masz te funkcje gdzieś u siebie:
|
||||
# from indicators import ema, rsi, atr, macd, adx_val
|
||||
from indicators import ema, rsi, atr, macd, adx_val
|
||||
|
||||
Signal = str # "BUY" | "SELL" | "NONE"
|
||||
|
||||
@dataclass
|
||||
class Decision:
|
||||
signal: Signal
|
||||
sl: float | None
|
||||
tp: float | None
|
||||
rpu: float # risk per unit (odległość od SL)
|
||||
|
||||
def evaluate_signal(df: pd.DataFrame) -> Decision:
|
||||
"""
|
||||
Warunki (poluzowane / opcjonalne):
|
||||
- (opcjonalnie) kierunek EMA200 (trend filter),
|
||||
- ADX >= CFG.adx_min,
|
||||
- ATR >= CFG.atr_min_frac_price * price,
|
||||
- RSI nieprzeciążone (BUY < rsi_buy_max, SELL > rsi_sell_min),
|
||||
- (opcjonalnie) MACD zgodny z kierunkiem.
|
||||
Wyjście: SL = k_ATR, TP = RR * R (R = dystans do SL).
|
||||
"""
|
||||
if df is None or len(df) < CFG.history_min_bars:
|
||||
return Decision("NONE", None, None, 0.0)
|
||||
|
||||
# Bezpieczne pobranie kolumn
|
||||
cols = {c.lower(): c for c in df.columns}
|
||||
C = pd.to_numeric(df[cols.get("close","Close")], errors="coerce").to_numpy(float)
|
||||
H = pd.to_numeric(df[cols.get("high","High")], errors="coerce").to_numpy(float)
|
||||
L = pd.to_numeric(df[cols.get("low","Low")], errors="coerce").to_numpy(float)
|
||||
|
||||
if len(C) == 0 or np.isnan(C[-1]):
|
||||
return Decision("NONE", None, None, 0.0)
|
||||
|
||||
# wskaźniki
|
||||
ema200 = ema(C, 200)
|
||||
rsi14 = rsi(C, CFG.rsi_len)
|
||||
atr14 = atr(H, L, C, 14)
|
||||
macd_line, macd_sig, macd_hist = macd(C, 12, 26, 9)
|
||||
adx14, pdi, mdi = adx_val(H, L, C, 14)
|
||||
|
||||
c = float(C[-1])
|
||||
a = float(atr14[-1]) if math.isfinite(atr14[-1]) else 0.0
|
||||
|
||||
if not (math.isfinite(c) and math.isfinite(a)) or a <= 0:
|
||||
return Decision("NONE", None, None, 0.0)
|
||||
|
||||
# filtry bazowe
|
||||
adx_ok = float(adx14[-1]) >= CFG.adx_min
|
||||
atr_ok = (a / max(1e-9, c)) >= CFG.atr_min_frac_price
|
||||
|
||||
# trend (opcjonalny)
|
||||
trend_ok_buy = True if not CFG.require_trend else (c > float(ema200[-1]))
|
||||
trend_ok_sell = True if not CFG.require_trend else (c < float(ema200[-1]))
|
||||
|
||||
# RSI „nieprzeciążone”
|
||||
rsi_val = float(rsi14[-1])
|
||||
rsi_ok_buy = rsi_val <= CFG.rsi_buy_max
|
||||
rsi_ok_sell = rsi_val >= CFG.rsi_sell_min
|
||||
|
||||
# MACD (opcjonalny)
|
||||
if CFG.use_macd_filter:
|
||||
macd_buy = (macd_line[-1] > macd_sig[-1] and macd_hist[-1] > 0)
|
||||
macd_sell = (macd_line[-1] < macd_sig[-1] and macd_hist[-1] < 0)
|
||||
else:
|
||||
macd_buy = macd_sell = True
|
||||
|
||||
signal: Signal = "NONE"
|
||||
sl = tp = None
|
||||
rpu = 0.0
|
||||
|
||||
if adx_ok and atr_ok:
|
||||
if trend_ok_buy and rsi_ok_buy and macd_buy:
|
||||
signal = "BUY"
|
||||
sl = c - CFG.sl_atr_mult * a
|
||||
risk = c - sl
|
||||
tp = c + CFG.tp_rr * risk
|
||||
rpu = max(1e-9, risk)
|
||||
elif CFG.allow_short and trend_ok_sell and rsi_ok_sell and macd_sell:
|
||||
signal = "SELL"
|
||||
sl = c + CFG.sl_atr_mult * a
|
||||
risk = sl - c
|
||||
tp = c - CFG.tp_rr * risk
|
||||
rpu = max(1e-9, risk)
|
||||
|
||||
return Decision(signal, sl, tp, rpu)
|
||||
@ -1,81 +1,114 @@
|
||||
<!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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<span>| Runda: <b id="roundNo">–</b></span>
|
||||
<span>| Gotówka: <b id="cash">–</b></span>
|
||||
</div>
|
||||
<!-- 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>
|
||||
|
||||
<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>
|
||||
<!-- WYKRES GOTÓWKI -->
|
||||
<section>
|
||||
<h2>Gotówka — wykres</h2>
|
||||
<div class="chart-wrap">
|
||||
<canvas id="cashChart" width="1200" height="300"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>Pozycje</h3>
|
||||
<table id="positions">
|
||||
<!-- Otwarte pozycje -->
|
||||
<section>
|
||||
<h2>Otwarte pozycje</h2>
|
||||
<table id="positions-table">
|
||||
<thead>
|
||||
<tr><th>Ticker</th><th>Strona</th><th>Ilość</th><th>Wejście</th><th>Ostatnia</th><th>PnL</th></tr>
|
||||
<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>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Transakcje (ostatnie 50)</h3>
|
||||
<table id="trades">
|
||||
</section>
|
||||
|
||||
<!-- Ostatnie sygnały -->
|
||||
<section>
|
||||
<h2>Ostatnie sygnały</h2>
|
||||
<table id="signals-table">
|
||||
<thead>
|
||||
<tr><th>Czas</th><th>Ticker</th><th>Akcja</th><th>Cena</th><th>Ilość</th></tr>
|
||||
<tr>
|
||||
<th>Ticker</th>
|
||||
<th>Time</th>
|
||||
<th>Price</th>
|
||||
<th>Signal</th>
|
||||
<th>Interval</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
<!-- 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
trade_log.json
Normal file
28
trade_log.json
Normal file
@ -0,0 +1,28 @@
|
||||
[
|
||||
{
|
||||
"time": "2025-08-15 12:31:18",
|
||||
"action": "SELL",
|
||||
"ticker": "OP-USD",
|
||||
"price": 0.7543854713439941,
|
||||
"qty": 3976.7467878924763,
|
||||
"side": -1,
|
||||
"pnl_abs": 0.0,
|
||||
"pnl_pct": 0.0,
|
||||
"realized_pnl_cum": 0.0,
|
||||
"cash_after": 10000.0,
|
||||
"reason": ""
|
||||
},
|
||||
{
|
||||
"time": "2025-08-15 12:32:18",
|
||||
"action": "SELL",
|
||||
"ticker": "OP-USD",
|
||||
"price": 0.7553843259811401,
|
||||
"qty": 3976.7467878924763,
|
||||
"side": -1,
|
||||
"pnl_abs": -3.972191969841845,
|
||||
"pnl_pct": -0.0013240639899472903,
|
||||
"realized_pnl_cum": -3.972191969841845,
|
||||
"cash_after": 9996.027808030158,
|
||||
"reason": "SIGNAL"
|
||||
}
|
||||
]
|
||||
264
trader.py
264
trader.py
@ -1,264 +0,0 @@
|
||||
# trader.py
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from config import CFG
|
||||
from strategy import evaluate_signal, Decision
|
||||
from portfolio import Portfolio
|
||||
|
||||
# ————————————————————————————————————————————————————————————————
|
||||
# save_outputs – opcjonalny helper; jeżeli masz własny w util.py, użyje jego
|
||||
try:
|
||||
from util import save_outputs # def save_outputs(root, symbols, trades_df, equity_df, cash): ...
|
||||
except Exception:
|
||||
def save_outputs(root_dir: str, symbols: List[str], trades_df: pd.DataFrame,
|
||||
equity_df: pd.DataFrame, cash: float):
|
||||
# fallback no-op, żeby nie wysypywać serwera
|
||||
pass
|
||||
# ————————————————————————————————————————————————————————————————
|
||||
# fetch_batch – Twój moduł pobierania danych (w logach: "data | Yahoo: ...")
|
||||
from data import fetch_batch # def fetch_batch(tickers: List[str], period: str, interval: str) -> Dict[str, pd.DataFrame]
|
||||
|
||||
log = logging.getLogger("trader")
|
||||
|
||||
|
||||
class TraderWorker:
|
||||
def __init__(self):
|
||||
self.portfolio = Portfolio(
|
||||
starting_cash=CFG.starting_cash,
|
||||
commission_per_trade=CFG.commission_per_trade,
|
||||
slippage_bp=CFG.slippage_bp,
|
||||
)
|
||||
|
||||
self.syms: List[str] = CFG.tickers[:] # 40 szt.
|
||||
self.hist: Dict[str, pd.DataFrame] = {}
|
||||
self.last_ts: Dict[str, pd.Timestamp] = {}
|
||||
|
||||
# stan pętli
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._stop_evt = threading.Event()
|
||||
self._running = False
|
||||
|
||||
# telemetry
|
||||
self._idx = 0
|
||||
self._round = 0
|
||||
self._stage = "idle"
|
||||
self._current_ticker = None
|
||||
self._last_action = None
|
||||
self._last_heartbeat = time.time()
|
||||
|
||||
log.info("INIT: %d symbols | period=%s interval=%s cash=%.2f",
|
||||
len(self.syms), CFG.yf_period, CFG.interval, self.portfolio.cash)
|
||||
|
||||
# ———————————————————— API do serwera ————————————————————
|
||||
|
||||
def is_running(self) -> bool:
|
||||
return self._running
|
||||
|
||||
def start(self) -> bool:
|
||||
if self._running:
|
||||
return False
|
||||
self._stop_evt.clear()
|
||||
self._thread = threading.Thread(target=self._loop, name="TraderLoop", daemon=True)
|
||||
self._thread.start()
|
||||
self._running = True
|
||||
log.info("LOOP: sequential 1-ticker | sleep=%ss", CFG.loop_sleep_s)
|
||||
log.info("LOOP: started")
|
||||
return True
|
||||
|
||||
def stop(self) -> bool:
|
||||
if not self._running:
|
||||
return False
|
||||
self._stop_evt.set()
|
||||
if self._thread and self._thread.is_alive():
|
||||
self._thread.join(timeout=5)
|
||||
self._running = False
|
||||
log.info("LOOP: stopped")
|
||||
return True
|
||||
|
||||
def _loop(self):
|
||||
while not self._stop_evt.is_set():
|
||||
self.tick_once()
|
||||
time.sleep(max(0, CFG.loop_sleep_s))
|
||||
|
||||
def status(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"running": self._running,
|
||||
"round": self._round,
|
||||
"stage": self._stage,
|
||||
"current_ticker": self._current_ticker,
|
||||
"current_index": self._idx % len(self.syms) if self.syms else 0,
|
||||
"tickers_total": len(self.syms),
|
||||
"last_action": self._last_action,
|
||||
"cash": float(self.portfolio.cash),
|
||||
"positions_count": len(self.portfolio.positions),
|
||||
"trades_count": len(self.portfolio.trades),
|
||||
"last_heartbeat": self._last_heartbeat,
|
||||
}
|
||||
|
||||
def list_positions(self) -> List[Dict[str, Any]]:
|
||||
out = []
|
||||
for tk, p in self.portfolio.positions.items():
|
||||
last_px = float(self.portfolio.last_price.get(tk, p.entry_price))
|
||||
if p.side == "long":
|
||||
pnl = (last_px - p.entry_price) * p.size
|
||||
else:
|
||||
pnl = (p.entry_price - last_px) * p.size
|
||||
out.append({
|
||||
"ticker": tk,
|
||||
"side": p.side,
|
||||
"size": float(p.size),
|
||||
"entry_price": float(p.entry_price),
|
||||
"last_price": last_px,
|
||||
"pnl": float(pnl),
|
||||
"sl": p.sl,
|
||||
"tp": p.tp,
|
||||
})
|
||||
return out
|
||||
|
||||
def list_trades(self) -> List[Dict[str, Any]]:
|
||||
return list(self.portfolio.trades)
|
||||
|
||||
def list_equity(self) -> List[List[float]]:
|
||||
# [ [ts_ms, equity], ... ]
|
||||
return [[int(ts), float(eq)] for (ts, eq) in self.portfolio.portfolio_equity]
|
||||
|
||||
# przyciski testowe z server.py
|
||||
def test_open_long(self, ticker: str, price: float, size: float):
|
||||
self.portfolio.last_price[ticker] = float(price)
|
||||
self.portfolio.open_long(ticker, float(size), float(price))
|
||||
|
||||
def test_open_short(self, ticker: str, price: float, size: float):
|
||||
self.portfolio.last_price[ticker] = float(price)
|
||||
self.portfolio.open_short(ticker, float(size), float(price))
|
||||
|
||||
def test_close(self, ticker: str, price: float | None):
|
||||
px = float(price) if price is not None else float(self.portfolio.last_price.get(ticker, 0.0))
|
||||
self.portfolio.close_all(ticker, px, reason="api:test_close")
|
||||
|
||||
# ———————————————————— Core ————————————————————
|
||||
|
||||
def _advance_index(self):
|
||||
if not self.syms:
|
||||
self._idx = 0
|
||||
return
|
||||
self._idx = (self._idx + 1) % len(self.syms)
|
||||
|
||||
def _equity_now(self) -> float:
|
||||
eq = float(self.portfolio.cash)
|
||||
for p in self.portfolio.positions.values():
|
||||
px = float(self.portfolio.last_price.get(p.ticker, p.entry_price))
|
||||
if p.side == "long":
|
||||
eq += (px - p.entry_price) * p.size
|
||||
else:
|
||||
eq += (p.entry_price - px) * p.size
|
||||
return float(eq)
|
||||
|
||||
def tick_once(self) -> float:
|
||||
t0 = time.time()
|
||||
self._round += 1
|
||||
if not self.syms:
|
||||
log.warning("No symbols configured")
|
||||
return 0.0
|
||||
|
||||
tk = self.syms[self._idx % len(self.syms)]
|
||||
self._current_ticker = tk
|
||||
self._stage = "fetch"
|
||||
log.info("[ROUND %d] TICKER %s (%d/%d) — stage=fetch",
|
||||
self._round, tk, (self._idx % len(self.syms)) + 1, len(self.syms))
|
||||
|
||||
try:
|
||||
# 1) POBIERZ DANE tylko dla jednego tickera
|
||||
batch = fetch_batch([tk], CFG.yf_period, CFG.interval)
|
||||
df = (batch or {}).get(tk)
|
||||
if df is None or len(df) == 0:
|
||||
log.warning("Data: %s -> EMPTY", tk)
|
||||
self._advance_index()
|
||||
return time.time() - t0
|
||||
|
||||
# sanity kolumn
|
||||
if "Close" not in df.columns or df["Close"].dropna().empty:
|
||||
log.warning("Data: %s -> no usable Close column (cols=%s)", tk, list(df.columns))
|
||||
self._advance_index()
|
||||
return time.time() - t0
|
||||
|
||||
last_close = float(pd.to_numeric(df["Close"], errors="coerce").dropna().iloc[-1])
|
||||
log.info("Data: %s -> bars=%d last_close=%.6f first=%s last=%s",
|
||||
tk, len(df), last_close, str(df.index[0]), str(df.index[-1]))
|
||||
|
||||
# 2) AKTUALIZUJ HISTORIĘ
|
||||
if tk not in self.hist:
|
||||
self.hist[tk] = df.copy()
|
||||
else:
|
||||
append = df[df.index > self.hist[tk].index.max()]
|
||||
if not append.empty:
|
||||
self.hist[tk] = pd.concat([self.hist[tk], append])
|
||||
|
||||
self.last_ts[tk] = df.index[-1]
|
||||
# aktualizuj „mark-to-market” dla equity
|
||||
self.portfolio.last_price[tk] = last_close
|
||||
|
||||
# 3) SYGNAŁ
|
||||
self._stage = "signal"
|
||||
try:
|
||||
closes_preview = list(
|
||||
pd.to_numeric(self.hist[tk].get("Close"), errors="coerce").dropna().tail(3).round(6))
|
||||
except Exception:
|
||||
closes_preview = []
|
||||
|
||||
decision: Decision = evaluate_signal(self.hist[tk])
|
||||
log.info("Signal: %s -> %s (last3=%s)", tk, decision, closes_preview)
|
||||
|
||||
# 3.5) SIZING NA RYZYKO (1R = dystans do SL)
|
||||
size = CFG.min_size
|
||||
if decision.signal != "NONE" and decision.rpu > 0:
|
||||
equity = self._equity_now()
|
||||
risk_cash = max(0.0, equity) * CFG.risk_per_trade_frac
|
||||
size = risk_cash / decision.rpu
|
||||
size = max(CFG.min_size, min(CFG.max_size, size))
|
||||
|
||||
# 4) EGZEKUCJA — TYLKO gdy BUY/SELL; NIE zamykamy przy NONE
|
||||
self._stage = "execute"
|
||||
action = "HOLD"
|
||||
if decision.signal == "BUY":
|
||||
self.portfolio.open_long(tk, size, last_close, sl=decision.sl, tp=decision.tp)
|
||||
action = "BUY"
|
||||
elif decision.signal == "SELL" and CFG.allow_short:
|
||||
self.portfolio.open_short(tk, size, last_close, sl=decision.sl, tp=decision.tp)
|
||||
action = "SELL"
|
||||
|
||||
self._last_action = action
|
||||
log.info("Action: %s %s @ %.6f (size=%.2f)", action, tk, last_close, float(size))
|
||||
|
||||
# 5) ZAPISZ WYJŚCIA (co rundę)
|
||||
self._stage = "save"
|
||||
# dociśnij próbkę equity dla UI (użyj timestampu ostatniej świecy)
|
||||
try:
|
||||
ts_ms = int(self.last_ts[tk].value / 1e6)
|
||||
self.portfolio.portfolio_equity.append((ts_ms, self._equity_now()))
|
||||
except Exception:
|
||||
# w ostateczności stempel teraz
|
||||
self.portfolio.portfolio_equity.append((int(time.time() * 1000), self._equity_now()))
|
||||
|
||||
trades_df = pd.DataFrame(self.portfolio.trades)
|
||||
eq_df = pd.DataFrame(self.portfolio.portfolio_equity, columns=["time", "equity"])
|
||||
save_outputs(CFG.root_dir, self.syms, trades_df, eq_df, self.portfolio.cash)
|
||||
log.debug("Saved outputs | trades=%d equity=%d cash=%.2f",
|
||||
len(trades_df), len(eq_df), self.portfolio.cash)
|
||||
|
||||
except Exception as e:
|
||||
log.exception("ERROR in tick_once(%s): %s", tk, e)
|
||||
|
||||
finally:
|
||||
took = time.time() - t0
|
||||
self._last_heartbeat = time.time()
|
||||
self._stage = "sleep"
|
||||
self._advance_index()
|
||||
log.info("[ROUND %d] done in %.2fs | next index=%d", self._round, took, self._idx % len(self.syms))
|
||||
return took
|
||||
Loading…
Reference in New Issue
Block a user