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
|
from __future__ import annotations
|
||||||
import time
|
import os, json, threading
|
||||||
from typing import Dict
|
from flask import Flask, jsonify, render_template, abort
|
||||||
import pandas as pd
|
from trading_bot import main as trading_bot_main # dostosuj jeśli moduł nazywa się inaczej
|
||||||
from config import CFG
|
|
||||||
from data import fetch_batch
|
|
||||||
from strategy import evaluate_signal
|
|
||||||
from portfolio import Portfolio
|
|
||||||
from io_utils import ensure_dirs, save_outputs
|
|
||||||
|
|
||||||
def interval_to_timedelta(interval: str) -> pd.Timedelta:
|
# Katalog projektu (tam gdzie leży ten plik)
|
||||||
# prosta mapka dla 1m/2m/5m/15m/30m/60m/1h
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
mapping = {
|
|
||||||
"1m":"1min","2m":"2min","5m":"5min","15m":"15min","30m":"30min",
|
# Domyślnie dane są w głównym katalogu projektu; można nadpisać przez env DATA_DIR
|
||||||
"60m":"60min","90m":"90min","1h":"60min"
|
DATA_DIR = os.environ.get("DATA_DIR", BASE_DIR)
|
||||||
}
|
|
||||||
key = mapping.get(interval, "1min")
|
def _load_json(name: str):
|
||||||
return pd.to_timedelta(key)
|
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__":
|
if __name__ == "__main__":
|
||||||
ensure_dirs(CFG.root_dir, CFG.tickers)
|
print(f"[Flask] DATA_DIR: {DATA_DIR}")
|
||||||
portfolio = Portfolio(starting_cash=CFG.starting_cash, commission_per_trade=1.0, slippage_bp=1.0)
|
start_trading_bot()
|
||||||
|
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)), debug=True)
|
||||||
last_ts: Dict[str, pd.Timestamp] = {}
|
|
||||||
hist: Dict[str, pd.DataFrame] = {}
|
|
||||||
bar_delta = interval_to_timedelta(CFG.interval)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
round_t0 = time.time()
|
|
||||||
batch = fetch_batch(CFG.tickers, CFG.yf_period, CFG.interval)
|
|
||||||
|
|
||||||
for tk, df in batch.items():
|
|
||||||
if df.empty:
|
|
||||||
continue
|
|
||||||
df = df.copy()
|
|
||||||
df.index = pd.to_datetime(df.index, utc=True)
|
|
||||||
|
|
||||||
prev = hist.get(tk)
|
|
||||||
if prev is not None and not prev.empty:
|
|
||||||
df_all = pd.concat([prev, df[~df.index.isin(prev.index)]], axis=0).sort_index()
|
|
||||||
else:
|
|
||||||
df_all = df.sort_index()
|
|
||||||
hist[tk] = df_all.tail(2000)
|
|
||||||
|
|
||||||
last = last_ts.get(tk)
|
|
||||||
new_part = df_all[df_all.index > last] if last is not None else df_all
|
|
||||||
|
|
||||||
if not new_part.empty:
|
|
||||||
for ts, row in new_part.iterrows():
|
|
||||||
o,h,l,c = float(row["open"]), float(row["high"]), float(row["low"]), float(row["close"])
|
|
||||||
|
|
||||||
# 1) egzekucja oczekujących na OPEN tego baraz
|
|
||||||
portfolio.on_new_bar(tk, ts, o,h,l,c)
|
|
||||||
|
|
||||||
# 2) sygnał na CLOSE → plan na KOLEJNY OPEN
|
|
||||||
df_upto = hist[tk].loc[:ts]
|
|
||||||
dec = evaluate_signal(df_upto)
|
|
||||||
portfolio.schedule_order(
|
|
||||||
tk, dec.signal, dec.rpu, dec.sl, dec.tp,
|
|
||||||
next_bar_ts=ts + pd.to_timedelta(1, unit="min"),
|
|
||||||
ref_price=float(df_upto["close"].iloc[-1])
|
|
||||||
)
|
|
||||||
|
|
||||||
last_ts[tk] = new_part.index[-1]
|
|
||||||
|
|
||||||
# zapis
|
|
||||||
import pandas as pd
|
|
||||||
trades_df = pd.DataFrame(portfolio.trades)
|
|
||||||
eq_df = pd.DataFrame(portfolio.portfolio_equity, columns=["time","equity"])
|
|
||||||
save_outputs(CFG.root_dir, CFG.tickers, trades_df, eq_df, portfolio.cash)
|
|
||||||
|
|
||||||
elapsed = time.time() - round_t0
|
|
||||||
sleep_s = max(0, 120 - elapsed)
|
|
||||||
print(f"Runda OK ({elapsed:.1f}s). Pauza {sleep_s:.1f}s.")
|
|
||||||
print(sleep_s)
|
|
||||||
time.sleep(sleep_s)
|
|
||||||
|
|||||||
57
config.py
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
|
from __future__ import annotations
|
||||||
import math
|
import math
|
||||||
from typing import Tuple
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
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:
|
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
|
w = np.ones(period) / period
|
||||||
out = np.convolve(arr, w, mode="valid")
|
out = np.convolve(arr, w, mode="valid")
|
||||||
return np.concatenate([np.full(period-1, np.nan), out])
|
return np.concatenate([np.full(period-1, np.nan), out])
|
||||||
|
|
||||||
def ema(arr: np.ndarray, period: int) -> np.ndarray:
|
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
|
arr = _as_1d(arr); n = len(arr)
|
||||||
for i,x in enumerate(arr):
|
out = np.full(n, np.nan, float)
|
||||||
e = x if math.isnan(e) else x*k + e*(1-k)
|
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
|
out[i] = e
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def rsi(arr: np.ndarray, period: int=14) -> np.ndarray:
|
def rsi(arr: np.ndarray, period: int = 14) -> np.ndarray:
|
||||||
if len(arr) < period+1: return np.full(len(arr), np.nan)
|
arr = _as_1d(arr); n = len(arr)
|
||||||
d = np.diff(arr, prepend=arr[0]); g = np.where(d>0,d,0.0); L = np.where(d<0,-d,0.0)
|
if n == 0: return np.array([], float)
|
||||||
ag, al = ema(g,period), ema(L,period); rs = ag/(al+1e-9)
|
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))
|
return 100 - (100 / (1 + rs))
|
||||||
|
|
||||||
def atr(H: np.ndarray, L: np.ndarray, C: np.ndarray, period: int=14) -> np.ndarray:
|
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)
|
H, L, C = _as_1d(H), _as_1d(L), _as_1d(C); n = len(C)
|
||||||
pc = np.roll(C,1)
|
if n == 0: return np.array([], float)
|
||||||
tr = np.maximum.reduce([H-L, np.abs(H-pc), np.abs(L-pc)])
|
if n < period+1: return np.full(n, np.nan)
|
||||||
tr[0] = H[0]-L[0]
|
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)
|
return ema(tr, period)
|
||||||
|
|
||||||
def macd(arr: np.ndarray, fast=12, slow=26, signal=9):
|
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
|
return line, sig, hist
|
||||||
|
|
||||||
def stoch_kd(H: np.ndarray, L: np.ndarray, C: np.ndarray, period=14, smooth=3):
|
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()
|
lowest = pd.Series(L).rolling(period, min_periods=1).min().to_numpy()
|
||||||
highest = pd.Series(H).rolling(period, min_periods=1).max().to_numpy()
|
highest = pd.Series(H).rolling(period, min_periods=1).max().to_numpy()
|
||||||
k = 100 * (C - lowest) / (highest - lowest + 1e-9)
|
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
|
return k, d
|
||||||
|
|
||||||
def bollinger(arr: np.ndarray, period=20, dev=2.0):
|
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)
|
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()
|
sd = s.rolling(period, min_periods=1).std(ddof=0).to_numpy()
|
||||||
return ma, ma + dev*sd, ma - dev*sd
|
return ma, ma + dev*sd, ma - dev*sd
|
||||||
|
|
||||||
def adx_val(H: np.ndarray, L: np.ndarray, C: np.ndarray, period=14):
|
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)
|
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)
|
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]
|
tr = np.maximum.reduce([tr1, tr2, tr3]); tr[0] = tr1[0]
|
||||||
atr14 = ema(tr, period)
|
atr14 = ema(tr, period)
|
||||||
pdi = 100 * ema(up_dm, period)/(atr14+1e-9)
|
pdi = 100 * ema(up_dm, period) / (atr14 + 1e-9)
|
||||||
mdi = 100 * ema(down_dm, period)/(atr14+1e-9)
|
mdi = 100 * ema(down_dm, period) / (atr14 + 1e-9)
|
||||||
dx = 100 * np.abs(pdi - mdi)/(pdi + mdi + 1e-9)
|
dx = 100 * np.abs(pdi - mdi) / (pdi + mdi + 1e-9)
|
||||||
adx = ema(dx, period)
|
adx = ema(dx, period)
|
||||||
return adx, pdi, mdi
|
return adx, pdi, mdi
|
||||||
|
|
||||||
def supertrend(H: np.ndarray, L: np.ndarray, C: np.ndarray, period=10, mult=3.0):
|
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()
|
a = atr(H, L, C, period).copy()
|
||||||
hl2 = (H + L)/2.0
|
hl2 = (H + L) / 2.0
|
||||||
ub = hl2 + mult*a
|
ub = hl2 + mult * a
|
||||||
lb = hl2 - mult*a
|
lb = hl2 - mult * a
|
||||||
n = len(C); st = np.full(n, np.nan)
|
st = np.full(n, np.nan)
|
||||||
for i in range(1, n):
|
for i in range(1, n):
|
||||||
prev = st[i-1] if not np.isnan(st[i-1]) else hl2[i-1]
|
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]
|
st[i] = ub[i] if C[i-1] <= prev else lb[i]
|
||||||
return st
|
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
|
# portfolio.py
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import math, time, json
|
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):
|
def _to_native(obj: Any):
|
||||||
"""Bezpieczne rzutowanie do typów akceptowalnych przez JSON."""
|
"""JSON-safe: NaN/Inf -> None; rekurencyjnie czyści struktury."""
|
||||||
if isinstance(obj, (float, int, str)) or obj is None:
|
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
|
return obj
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
return {k: _to_native(v) for k, v in obj.items()}
|
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:
|
def save_json(path: str, data: Any) -> None:
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
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:
|
class Portfolio:
|
||||||
"""
|
"""
|
||||||
Prosta symulacja portfela:
|
LONG open: cash -= qty*price ; close: cash += qty*price ; PnL = (px - entry)*qty
|
||||||
- cash
|
SHORT open (stock): cash += qty*price ; close: cash -= qty*price ; PnL = (entry - px)*qty
|
||||||
- positions: {ticker: {"qty": float, "entry": float, "side": int}}
|
SHORT open (margin): cash bez zmian ; close: cash += PnL ; PnL = (entry - px)*qty
|
||||||
- equity = cash + niezrealizowany PnL
|
|
||||||
Logi transakcji z realized PnL (wartość i %), plus PnL skumulowany.
|
total_value:
|
||||||
Zapis tylko w JSON.
|
- stock -> cash + positions_net (wartość likwidacyjna)
|
||||||
|
- margin -> start_capital + realized_pnl + unrealized_pnl
|
||||||
"""
|
"""
|
||||||
def __init__(self, capital: float):
|
def __init__(self, capital: float):
|
||||||
self.cash = float(capital)
|
self.cash = float(capital)
|
||||||
@ -35,103 +64,270 @@ class Portfolio:
|
|||||||
self.positions: Dict[str, Dict] = {}
|
self.positions: Dict[str, Dict] = {}
|
||||||
self.history: List[Dict] = []
|
self.history: List[Dict] = []
|
||||||
self.trade_log: 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:
|
# ---------- helpers ----------
|
||||||
unreal = 0.0
|
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():
|
for t, p in self.positions.items():
|
||||||
price = prices.get(t)
|
px = None
|
||||||
if price is not None and not math.isnan(price):
|
if prices is not None:
|
||||||
unreal += (price - p["entry"]) * p["qty"] * p["side"]
|
px = prices.get(t)
|
||||||
return self.cash + unreal
|
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,
|
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")
|
ts = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
side_str = "LONG" if side == 1 else "SHORT"
|
|
||||||
if action == "BUY":
|
if action == "BUY":
|
||||||
print(f"[{ts}] BUY {ticker} @ {price:.6f}, qty={qty:.6f}, side={side_str}, cash={self.cash:.2f}")
|
print(f"[{ts}] BUY {ticker} @ {price:.6f}, qty={qty:.6f}, side={'LONG' if side==1 else 'SHORT'}, cash={self.cash:.2f}")
|
||||||
elif action == "SELL":
|
else:
|
||||||
print(f"[{ts}] SELL {ticker} @ {price:.6f}, qty={qty:.6f}, "
|
r = f", reason={reason}" if reason else ""
|
||||||
f"PnL={pnl_abs:+.2f} ({pnl_pct:+.2%}), "
|
print(f"[{ts}] SELL {ticker} @ {price:.6f}, qty={qty:.6f}, PnL={pnl_abs:+.2f} ({pnl_pct:+.2%}), cash={self.cash:.2f}{r}")
|
||||||
f"cumPnL={self.realized_pnl:+.2f}, cash={self.cash:.2f}")
|
|
||||||
# zapis do logu w pamięci
|
|
||||||
self.trade_log.append({
|
self.trade_log.append({
|
||||||
"time": ts,
|
"time": ts, "action": action, "ticker": ticker, "price": float(price),
|
||||||
"action": action,
|
"qty": float(qty), "side": int(side),
|
||||||
"ticker": ticker,
|
"pnl_abs": float(pnl_abs), "pnl_pct": float(pnl_pct),
|
||||||
"price": float(price),
|
"realized_pnl_cum": float(self.realized_pnl), "cash_after": float(self.cash),
|
||||||
"qty": float(qty),
|
"reason": reason or ""
|
||||||
"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)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# ---------- główna aktualizacja ----------
|
||||||
def on_signals(self, sigs: List[dict]):
|
def on_signals(self, sigs: List[dict]):
|
||||||
"""
|
# normalizacja
|
||||||
sigs: lista dictów/obiektów z polami: ticker, price, signal, time
|
clean: List[Dict[str, Any]] = []
|
||||||
BUY (1) / SELL (-1) / HOLD (0)
|
|
||||||
"""
|
|
||||||
clean = []
|
|
||||||
for s in sigs:
|
for s in sigs:
|
||||||
if isinstance(s, dict):
|
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)
|
clean.append(s)
|
||||||
else:
|
else:
|
||||||
if getattr(s, "error", None) is None and not math.isnan(getattr(s, "price", float("nan"))):
|
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:
|
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({
|
self.history.append({
|
||||||
"time": time.strftime("%Y-%m-%d %H:%M:%S"),
|
"time": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"equity": self.cash,
|
"cash": float(self.cash),
|
||||||
"cash": self.cash,
|
"positions_value": float(vals["positions_gross"]),
|
||||||
"open_positions": len(self.positions)
|
"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)
|
save_json("portfolio_history.json", self.history)
|
||||||
return
|
return
|
||||||
|
|
||||||
prices = {s["ticker"]: s["price"] for s in clean}
|
# mapy cen
|
||||||
n = len(clean)
|
prices = {s["ticker"]: float(s["price"]) for s in clean}
|
||||||
per_trade_cash = max(self.cash / (n * 2), 0.0)
|
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:
|
for s in clean:
|
||||||
t, sig, price = s["ticker"], int(s["signal"]), float(s["price"])
|
t, sig = s["ticker"], int(s["signal"])
|
||||||
|
price = float(s["price"])
|
||||||
# zamknięcie lub odwrócenie
|
|
||||||
if t in self.positions:
|
if t in self.positions:
|
||||||
pos = self.positions[t]
|
pos = self.positions[t]
|
||||||
if sig == 0 or sig != pos["side"]:
|
if sig == 0 or sig != pos["side"]:
|
||||||
qty = pos["qty"]
|
self._close_position(t, price, reason="SIGNAL")
|
||||||
entry = pos["entry"]
|
|
||||||
side = pos["side"]
|
# 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
|
self.cash += qty * price
|
||||||
pnl_abs = (price - entry) * qty * side
|
# margin: brak zmiany cash przy short open
|
||||||
denom = max(qty * entry, 1e-12)
|
|
||||||
pnl_pct = pnl_abs / denom
|
|
||||||
self.realized_pnl += pnl_abs
|
|
||||||
self._log_trade("SELL", t, price, qty, side, pnl_abs, pnl_pct)
|
|
||||||
del self.positions[t]
|
|
||||||
|
|
||||||
# otwarcie
|
self.positions[t] = {
|
||||||
if t not in self.positions and sig != 0 and per_trade_cash > 0:
|
"qty": qty, "entry": price, "side": sig,
|
||||||
qty = per_trade_cash / price
|
"stop": stop, "take": take,
|
||||||
self.cash -= qty * price
|
"trail_best": trail_best, "trail_stop": trail_stop,
|
||||||
self.positions[t] = {"qty": qty, "entry": price, "side": sig}
|
"one_r": one_r, "be_done": False
|
||||||
self._log_trade("BUY", t, price, qty, sig)
|
}
|
||||||
|
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({
|
self.history.append({
|
||||||
"time": clean[0]["time"],
|
"time": clean[0]["time"],
|
||||||
"equity": float(equity),
|
|
||||||
"cash": float(self.cash),
|
"cash": float(self.cash),
|
||||||
"open_positions": int(len(self.positions)),
|
"positions_value": float(vals["positions_gross"]),
|
||||||
"realized_pnl_cum": float(self.realized_pnl)
|
"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("trade_log.json", self.trade_log)
|
||||||
save_json("portfolio_history.json", self.history)
|
save_json("portfolio_history.json", self.history)
|
||||||
save_json("positions.json", [{"ticker": t, **p} for t, p in self.positions.items()])
|
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",
|
"ticker": "EURUSD=X",
|
||||||
"time": "2025-08-15 10:17:00+00:00",
|
"time": "2025-08-15 10:30:00+00:00",
|
||||||
"price": 1.1691803932189941,
|
"price": 1.1689070463180542,
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "CHFJPY=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 182.3179931640625,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "DOGE-USD",
|
|
||||||
"time": "2025-08-15 10:15:00+00:00",
|
|
||||||
"price": 0.2317201942205429,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "OP-USD",
|
|
||||||
"time": "2025-08-15 10:11:00+00:00",
|
|
||||||
"price": 0.7549265623092651,
|
|
||||||
"signal": -1,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "BTC-USD",
|
|
||||||
"time": "2025-08-15 10:15:00+00:00",
|
|
||||||
"price": 118967.421875,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "NZDUSD=X",
|
|
||||||
"time": "2025-08-15 10:16:00+00:00",
|
|
||||||
"price": 0.5927330851554871,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "EURAUD=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 1.7958300113677979,
|
|
||||||
"signal": 0,
|
"signal": 0,
|
||||||
"period": "7d",
|
"period": "7d",
|
||||||
"interval": "1m",
|
"interval": "1m",
|
||||||
@ -73,215 +10,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ticker": "USDCHF=X",
|
"ticker": "USDCHF=X",
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
"time": "2025-08-15 10:31:00+00:00",
|
||||||
"price": 0.8054800033569336,
|
"price": 0.8058599829673767,
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "SOL-USD",
|
|
||||||
"time": "2025-08-15 10:15:00+00:00",
|
|
||||||
"price": 195.40365600585938,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "USDJPY=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 146.86399841308594,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "GBPJPY=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 199.14500427246094,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "XLM-USD",
|
|
||||||
"time": "2025-08-15 10:16:00+00:00",
|
|
||||||
"price": 0.42842021584510803,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "EURGBP=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 0.8619400262832642,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "LTC-USD",
|
|
||||||
"time": "2025-08-15 10:14:00+00:00",
|
|
||||||
"price": 121.15275573730469,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "USDCAD=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 1.3791099786758423,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "BNB-USD",
|
|
||||||
"time": "2025-08-15 10:15:00+00:00",
|
|
||||||
"price": 844.4680786132812,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "ETH-USD",
|
|
||||||
"time": "2025-08-15 10:16:00+00:00",
|
|
||||||
"price": 4635.28955078125,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "ETC-USD",
|
|
||||||
"time": "2025-08-15 10:15:00+00:00",
|
|
||||||
"price": 22.40416717529297,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "CADJPY=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 106.49600219726562,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "TRX-USD",
|
|
||||||
"time": "2025-08-15 10:16:00+00:00",
|
|
||||||
"price": 0.3588825762271881,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "EURJPY=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 171.65899658203125,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "TON-USD",
|
|
||||||
"time": "2025-08-15 10:11:00+00:00",
|
|
||||||
"price": 0.01708882860839367,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "ADA-USD",
|
|
||||||
"time": "2025-08-15 10:16:00+00:00",
|
|
||||||
"price": 0.951282799243927,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "NZDJPY=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 87.01699829101562,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "AUDJPY=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 95.58399963378906,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "EURCAD=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 1.611799955368042,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "BCH-USD",
|
|
||||||
"time": "2025-08-15 10:15:00+00:00",
|
|
||||||
"price": 596.3115844726562,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "DOT-USD",
|
|
||||||
"time": "2025-08-15 10:15:00+00:00",
|
|
||||||
"price": 4.005911827087402,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "ATOM-USD",
|
|
||||||
"time": "2025-08-15 10:16:00+00:00",
|
|
||||||
"price": 4.517627239227295,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "AUDNZD=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 1.0983599424362183,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "GC=F",
|
|
||||||
"time": "2025-08-15 10:08:00+00:00",
|
|
||||||
"price": 3386.10009765625,
|
|
||||||
"signal": 0,
|
"signal": 0,
|
||||||
"period": "7d",
|
"period": "7d",
|
||||||
"interval": "1m",
|
"interval": "1m",
|
||||||
@ -289,44 +19,35 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ticker": "NEAR-USD",
|
"ticker": "NEAR-USD",
|
||||||
"time": "2025-08-15 10:11:00+00:00",
|
"time": "2025-08-15 10:21:00+00:00",
|
||||||
"price": 2.7867467403411865,
|
"price": 2.7824549674987793,
|
||||||
"signal": -1,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "GBPCAD=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 1.8698300123214722,
|
|
||||||
"signal": 0,
|
"signal": 0,
|
||||||
"period": "7d",
|
"period": "7d",
|
||||||
"interval": "1m",
|
"interval": "1m",
|
||||||
"error": null
|
"error": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ticker": "XRP-USD",
|
"ticker": "USDJPY=X",
|
||||||
"time": "2025-08-15 10:16:00+00:00",
|
"time": "2025-08-15 10:31:00+00:00",
|
||||||
"price": 3.111118793487549,
|
"price": 146.8820037841797,
|
||||||
"signal": 0,
|
"signal": 0,
|
||||||
"period": "7d",
|
"period": "7d",
|
||||||
"interval": "1m",
|
"interval": "1m",
|
||||||
"error": null
|
"error": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ticker": "EURCHF=X",
|
"ticker": "ETH-USD",
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
"time": "2025-08-15 10:28:00+00:00",
|
||||||
"price": 0.9409899711608887,
|
"price": 4645.15771484375,
|
||||||
"signal": 0,
|
"signal": 0,
|
||||||
"period": "7d",
|
"period": "7d",
|
||||||
"interval": "1m",
|
"interval": "1m",
|
||||||
"error": null
|
"error": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ticker": "LINK-USD",
|
"ticker": "CADJPY=X",
|
||||||
"time": "2025-08-15 10:16:00+00:00",
|
"time": "2025-08-15 10:31:00+00:00",
|
||||||
"price": 22.371389389038086,
|
"price": 106.47799682617188,
|
||||||
"signal": 0,
|
"signal": 0,
|
||||||
"period": "7d",
|
"period": "7d",
|
||||||
"interval": "1m",
|
"interval": "1m",
|
||||||
@ -334,8 +55,287 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ticker": "AUDUSD=X",
|
"ticker": "AUDUSD=X",
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
"time": "2025-08-15 10:31:00+00:00",
|
||||||
"price": 0.6509992480278015,
|
"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,
|
"signal": 0,
|
||||||
"period": "7d",
|
"period": "7d",
|
||||||
"interval": "1m",
|
"interval": "1m",
|
||||||
@ -1,110 +1,22 @@
|
|||||||
{
|
{
|
||||||
"time": "2025-08-15 12:19:02",
|
"time": "2025-08-15 12:32:18",
|
||||||
"last_history": {
|
"last_history": {
|
||||||
"time": "2025-08-15 10:17:00+00:00",
|
"time": "2025-08-15 10:30:00+00:00",
|
||||||
"cash": 10000.0,
|
"cash": 9996.027808030158,
|
||||||
"positions_value": 5999.999999999975,
|
"positions_value": 0.0,
|
||||||
"positions_net": -5999.999999999975,
|
"positions_net": 0.0,
|
||||||
"total_value": 10000.0,
|
"total_value": 9996.027808030158,
|
||||||
"equity_from_start": 10000.0,
|
"equity_from_start": 9996.027808030158,
|
||||||
"unrealized_pnl": 0.0,
|
"unrealized_pnl": 0.0,
|
||||||
"realized_pnl_cum": 0.0,
|
"realized_pnl_cum": -3.972191969841845,
|
||||||
"open_positions": 2
|
"open_positions": 0
|
||||||
},
|
},
|
||||||
"positions": [
|
"positions": [],
|
||||||
{
|
|
||||||
"ticker": "OP-USD",
|
|
||||||
"qty": 3973.896468582607,
|
|
||||||
"entry": 0.7549265623092651,
|
|
||||||
"side": -1,
|
|
||||||
"stop": 0.7624758279323578,
|
|
||||||
"take": 0.7398280310630798,
|
|
||||||
"trail_best": 0.7549265623092651,
|
|
||||||
"trail_stop": 0.7624758279323578,
|
|
||||||
"one_r": 0.00754926562309266,
|
|
||||||
"be_done": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "NEAR-USD",
|
|
||||||
"qty": 1076.5240904642392,
|
|
||||||
"entry": 2.7867467403411865,
|
|
||||||
"side": -1,
|
|
||||||
"stop": 2.8146142077445986,
|
|
||||||
"take": 2.731011805534363,
|
|
||||||
"trail_best": 2.7867467403411865,
|
|
||||||
"trail_stop": 2.8146142077445986,
|
|
||||||
"one_r": 0.02786746740341206,
|
|
||||||
"be_done": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"signals": [
|
"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",
|
"ticker": "EURUSD=X",
|
||||||
"time": "2025-08-15 10:17:00+00:00",
|
"time": "2025-08-15 10:30:00+00:00",
|
||||||
"price": 1.1691803932189941,
|
"price": 1.1689070463180542,
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "CHFJPY=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 182.3179931640625,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "DOGE-USD",
|
|
||||||
"time": "2025-08-15 10:15:00+00:00",
|
|
||||||
"price": 0.2317201942205429,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "OP-USD",
|
|
||||||
"time": "2025-08-15 10:11:00+00:00",
|
|
||||||
"price": 0.7549265623092651,
|
|
||||||
"signal": -1,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "BTC-USD",
|
|
||||||
"time": "2025-08-15 10:15:00+00:00",
|
|
||||||
"price": 118967.421875,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "NZDUSD=X",
|
|
||||||
"time": "2025-08-15 10:16:00+00:00",
|
|
||||||
"price": 0.5927330851554871,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "EURAUD=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 1.7958300113677979,
|
|
||||||
"signal": 0,
|
"signal": 0,
|
||||||
"period": "7d",
|
"period": "7d",
|
||||||
"interval": "1m",
|
"interval": "1m",
|
||||||
@ -112,215 +24,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ticker": "USDCHF=X",
|
"ticker": "USDCHF=X",
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
"time": "2025-08-15 10:31:00+00:00",
|
||||||
"price": 0.8054800033569336,
|
"price": 0.8058599829673767,
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "SOL-USD",
|
|
||||||
"time": "2025-08-15 10:15:00+00:00",
|
|
||||||
"price": 195.40365600585938,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "USDJPY=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 146.86399841308594,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "GBPJPY=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 199.14500427246094,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "XLM-USD",
|
|
||||||
"time": "2025-08-15 10:16:00+00:00",
|
|
||||||
"price": 0.42842021584510803,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "EURGBP=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 0.8619400262832642,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "LTC-USD",
|
|
||||||
"time": "2025-08-15 10:14:00+00:00",
|
|
||||||
"price": 121.15275573730469,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "USDCAD=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 1.3791099786758423,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "BNB-USD",
|
|
||||||
"time": "2025-08-15 10:15:00+00:00",
|
|
||||||
"price": 844.4680786132812,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "ETH-USD",
|
|
||||||
"time": "2025-08-15 10:16:00+00:00",
|
|
||||||
"price": 4635.28955078125,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "ETC-USD",
|
|
||||||
"time": "2025-08-15 10:15:00+00:00",
|
|
||||||
"price": 22.40416717529297,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "CADJPY=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 106.49600219726562,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "TRX-USD",
|
|
||||||
"time": "2025-08-15 10:16:00+00:00",
|
|
||||||
"price": 0.3588825762271881,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "EURJPY=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 171.65899658203125,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "TON-USD",
|
|
||||||
"time": "2025-08-15 10:11:00+00:00",
|
|
||||||
"price": 0.01708882860839367,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "ADA-USD",
|
|
||||||
"time": "2025-08-15 10:16:00+00:00",
|
|
||||||
"price": 0.951282799243927,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "NZDJPY=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 87.01699829101562,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "AUDJPY=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 95.58399963378906,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "EURCAD=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 1.611799955368042,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "BCH-USD",
|
|
||||||
"time": "2025-08-15 10:15:00+00:00",
|
|
||||||
"price": 596.3115844726562,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "DOT-USD",
|
|
||||||
"time": "2025-08-15 10:15:00+00:00",
|
|
||||||
"price": 4.005911827087402,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "ATOM-USD",
|
|
||||||
"time": "2025-08-15 10:16:00+00:00",
|
|
||||||
"price": 4.517627239227295,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "AUDNZD=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 1.0983599424362183,
|
|
||||||
"signal": 0,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "GC=F",
|
|
||||||
"time": "2025-08-15 10:08:00+00:00",
|
|
||||||
"price": 3386.10009765625,
|
|
||||||
"signal": 0,
|
"signal": 0,
|
||||||
"period": "7d",
|
"period": "7d",
|
||||||
"interval": "1m",
|
"interval": "1m",
|
||||||
@ -328,44 +33,35 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ticker": "NEAR-USD",
|
"ticker": "NEAR-USD",
|
||||||
"time": "2025-08-15 10:11:00+00:00",
|
"time": "2025-08-15 10:21:00+00:00",
|
||||||
"price": 2.7867467403411865,
|
"price": 2.7824549674987793,
|
||||||
"signal": -1,
|
|
||||||
"period": "7d",
|
|
||||||
"interval": "1m",
|
|
||||||
"error": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ticker": "GBPCAD=X",
|
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
|
||||||
"price": 1.8698300123214722,
|
|
||||||
"signal": 0,
|
"signal": 0,
|
||||||
"period": "7d",
|
"period": "7d",
|
||||||
"interval": "1m",
|
"interval": "1m",
|
||||||
"error": null
|
"error": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ticker": "XRP-USD",
|
"ticker": "USDJPY=X",
|
||||||
"time": "2025-08-15 10:16:00+00:00",
|
"time": "2025-08-15 10:31:00+00:00",
|
||||||
"price": 3.111118793487549,
|
"price": 146.8820037841797,
|
||||||
"signal": 0,
|
"signal": 0,
|
||||||
"period": "7d",
|
"period": "7d",
|
||||||
"interval": "1m",
|
"interval": "1m",
|
||||||
"error": null
|
"error": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ticker": "EURCHF=X",
|
"ticker": "ETH-USD",
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
"time": "2025-08-15 10:28:00+00:00",
|
||||||
"price": 0.9409899711608887,
|
"price": 4645.15771484375,
|
||||||
"signal": 0,
|
"signal": 0,
|
||||||
"period": "7d",
|
"period": "7d",
|
||||||
"interval": "1m",
|
"interval": "1m",
|
||||||
"error": null
|
"error": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ticker": "LINK-USD",
|
"ticker": "CADJPY=X",
|
||||||
"time": "2025-08-15 10:16:00+00:00",
|
"time": "2025-08-15 10:31:00+00:00",
|
||||||
"price": 22.371389389038086,
|
"price": 106.47799682617188,
|
||||||
"signal": 0,
|
"signal": 0,
|
||||||
"period": "7d",
|
"period": "7d",
|
||||||
"interval": "1m",
|
"interval": "1m",
|
||||||
@ -373,8 +69,287 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ticker": "AUDUSD=X",
|
"ticker": "AUDUSD=X",
|
||||||
"time": "2025-08-15 10:18:00+00:00",
|
"time": "2025-08-15 10:31:00+00:00",
|
||||||
"price": 0.6509992480278015,
|
"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,
|
"signal": 0,
|
||||||
"period": "7d",
|
"period": "7d",
|
||||||
"interval": "1m",
|
"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 ──────────────────────────────────────────────────────────────────────
|
async function getJSON(url) {
|
||||||
const fmtNum = (x, d = 2) =>
|
const r = await fetch(url, { cache: "no-store" });
|
||||||
x === null || x === undefined || Number.isNaN(x) ? "–" : Number(x).toFixed(d);
|
if (!r.ok) throw new Error(`${url}: ${r.status}`);
|
||||||
|
return r.json();
|
||||||
// Potencjalne adresy backendu (API). Pierwszy to aktualny origin UI.
|
|
||||||
const apiCandidates = [
|
|
||||||
window.location.origin,
|
|
||||||
"http://127.0.0.1:8000",
|
|
||||||
"http://localhost:8000",
|
|
||||||
"http://172.27.20.120:8000", // z logów serwera
|
|
||||||
];
|
|
||||||
|
|
||||||
let API_BASE = null;
|
|
||||||
let warnedMixed = false;
|
|
||||||
|
|
||||||
function withTimeout(ms = 6000) {
|
|
||||||
const ctrl = new AbortController();
|
|
||||||
const id = setTimeout(() => ctrl.abort("timeout"), ms);
|
|
||||||
return { signal: ctrl.signal, done: () => clearTimeout(id) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeUrl(path) {
|
// prosty wykres linii na canvas (bez bibliotek)
|
||||||
return `${API_BASE}${path}${path.includes("?") ? "&" : "?"}_ts=${Date.now()}`;
|
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) {
|
if (!points || points.length === 0) {
|
||||||
const el = document.getElementById(id);
|
ctx.fillStyle = "#9aa3b2";
|
||||||
if (!el) return;
|
ctx.fillText("Brak danych", 10, 20);
|
||||||
el.textContent = text;
|
return;
|
||||||
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."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 loadAll() {
|
||||||
async function pickBackend() {
|
|
||||||
for (const base of apiCandidates) {
|
|
||||||
try {
|
|
||||||
const t = withTimeout(2500);
|
|
||||||
const r = await fetch(`${base}/api/status?_ts=${Date.now()}`, {
|
|
||||||
cache: "no-store",
|
|
||||||
signal: t.signal,
|
|
||||||
});
|
|
||||||
t.done();
|
|
||||||
if (r.ok) {
|
|
||||||
API_BASE = base;
|
|
||||||
console.debug("[api] using", API_BASE);
|
|
||||||
warnMixedContent(API_BASE);
|
|
||||||
setBadgeTitle("loopState", `API: ${API_BASE}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.debug("[api] probe", base, "->", r.status);
|
|
||||||
} catch (e) {
|
|
||||||
// ignorujemy i próbujemy kolejny kandydat
|
|
||||||
console.debug("[api] probe fail", base, e?.message || e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error("Nie znaleziono działającego backendu (API_BASE). Upewnij się, że server.py działa na porcie 8000.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── API helpers ───────────────────────────────────────────────────────────────
|
|
||||||
async function apiGet(path) {
|
|
||||||
const url = makeUrl(path);
|
|
||||||
const t0 = performance.now();
|
|
||||||
const t = withTimeout(6000);
|
|
||||||
try {
|
try {
|
||||||
const r = await fetch(url, { cache: "no-store", signal: t.signal });
|
const [snap, hist, pos, trades] = await Promise.all([
|
||||||
const t1 = performance.now();
|
getJSON("/api/snapshot"),
|
||||||
console.debug("[api] GET", url, r.status, (t1 - t0).toFixed(1) + "ms");
|
getJSON("/api/history"),
|
||||||
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
|
getJSON("/api/positions"),
|
||||||
return r.json();
|
getJSON("/api/trades"),
|
||||||
} finally {
|
]);
|
||||||
t.done();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiPost(path, body) {
|
// czas
|
||||||
const url = makeUrl(path);
|
document.getElementById("last-update").textContent =
|
||||||
const t0 = performance.now();
|
"Ostatnia aktualizacja: " + (snap?.last_history?.time || "—");
|
||||||
const t = withTimeout(6000);
|
|
||||||
try {
|
// ===== ostatni wiersz historii =====
|
||||||
const r = await fetch(url, {
|
const last = Array.isArray(hist) && hist.length ? hist[hist.length - 1] : null;
|
||||||
method: "POST",
|
|
||||||
headers: body ? { "Content-Type": "application/json" } : undefined,
|
const cashVal = Number(last?.cash ?? 0);
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
const totalVal = Number(
|
||||||
signal: t.signal,
|
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 ────────────────────────────────────────────────────────────────
|
// Zysk = suma niezrealizowanych PnL na otwartych pozycjach
|
||||||
async function refreshStatus() {
|
let unrealPnL = 0;
|
||||||
try {
|
(pos || []).forEach(p => {
|
||||||
const s = await apiGet("/api/status");
|
const price = Number(lastPrice.get(p.ticker));
|
||||||
setBadge("loopState", s.running ? "RUNNING" : "STOPPED", s.running);
|
const entry = Number(p.entry);
|
||||||
setBadgeTitle("loopState", `API: ${API_BASE} | last_action=${s.last_action || "–"}`);
|
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");
|
// liczba otwartych pozycji
|
||||||
if (roundEl) roundEl.textContent = s.round ?? "–";
|
document.getElementById("open-pos").textContent =
|
||||||
|
Number(last?.open_positions ?? (pos?.length ?? 0));
|
||||||
|
|
||||||
const cashEl = document.getElementById("cash");
|
// ===== WYKRES WARTOŚCI KONTA (TOTAL) =====
|
||||||
if (cashEl) cashEl.textContent = fmtNum(s.cash, 2);
|
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
|
// ===== WYKRES GOTÓWKI (CASH) =====
|
||||||
const stageEl = document.getElementById("stage");
|
const cashCanvas = document.getElementById("cashChart");
|
||||||
if (stageEl) stageEl.textContent = (s.stage || "idle").toUpperCase();
|
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");
|
// ===== POZYCJE =====
|
||||||
if (tickerEl) tickerEl.textContent = s.current_ticker || "–";
|
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;
|
let upnl = NaN, upct = NaN;
|
||||||
const total = s.tickers_total ?? 0;
|
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");
|
const tr = document.createElement("tr");
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td>${p.ticker}</td>
|
<td>${p.ticker}</td>
|
||||||
<td>${p.side}</td>
|
<td>${fmt6(qty)}</td>
|
||||||
<td class="num">${fmtNum(p.size, 0)}</td>
|
<td>${fmt6(entry)}</td>
|
||||||
<td class="num">${fmtNum(p.entry_price)}</td>
|
<td>${side === 1 ? "LONG" : "SHORT"}</td>
|
||||||
<td class="num">${fmtNum(p.last_price)}</td>
|
<td>${fmt6(price)}</td>
|
||||||
<td class="num">${fmtNum(p.pnl)}</td>
|
<td class="${pnlClass}">${fmt2(upnl)}</td>
|
||||||
|
<td class="${pnlClass}">${fmtPct(upct)}</td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(tr);
|
posBody.appendChild(tr);
|
||||||
}
|
});
|
||||||
} catch (e) {
|
|
||||||
console.error("positions error:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshTrades() {
|
// ===== SYGNAŁY =====
|
||||||
try {
|
const sigBody = document.querySelector("#signals-table tbody");
|
||||||
const { trades } = await apiGet("/api/trades");
|
sigBody.innerHTML = "";
|
||||||
const tbody = document.querySelector("#trades tbody");
|
const signals = (snap?.signals || []).slice().sort((a,b)=>a.ticker.localeCompare(b.ticker));
|
||||||
if (!tbody) return;
|
signals.forEach((s) => {
|
||||||
tbody.innerHTML = "";
|
const sigTxt = s.signal === 1 ? "BUY" : (s.signal === -1 ? "SELL" : "HOLD");
|
||||||
trades.slice(-50).reverse().forEach((t) => {
|
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td>${t.time ?? ""}</td>
|
<td>${s.ticker}</td>
|
||||||
<td>${t.ticker ?? ""}</td>
|
<td>${s.time}</td>
|
||||||
<td>${t.action ?? ""}</td>
|
<td>${fmt6(s.price)}</td>
|
||||||
<td class="num">${fmtNum(t.price)}</td>
|
<td class="${sigTxt}">${sigTxt}</td>
|
||||||
<td class="num">${fmtNum(t.size, 0)}</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) {
|
} catch (e) {
|
||||||
console.error("trades error:", e);
|
console.error("loadAll error:", e);
|
||||||
|
document.getElementById("last-update").textContent = "Błąd ładowania danych";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshAll() {
|
document.getElementById("refresh-btn").addEventListener("click", loadAll);
|
||||||
await Promise.all([refreshStatus(), refreshPositions(), refreshTrades()]);
|
loadAll();
|
||||||
}
|
setInterval(loadAll, 1000);
|
||||||
|
|
||||||
// ── 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.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
@ -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>
|
<!doctype html>
|
||||||
<html lang="pl">
|
<html lang="pl">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Trader – Panel</title>
|
<title>Trading Dashboard</title>
|
||||||
<link rel="stylesheet" href="/static/style.css"/>
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
<style>
|
|
||||||
body { font-family: system-ui, Arial, sans-serif; margin: 0; padding: 1rem; background: #fafafa; }
|
|
||||||
.toolbar { display:flex; gap:.5rem; flex-wrap:wrap; align-items:center; margin-bottom:1rem; }
|
|
||||||
.badge { padding:.2rem .5rem; border-radius:.5rem; background:#ddd; font-weight:600; }
|
|
||||||
.badge.ok { background:#d1fae5; }
|
|
||||||
.badge.err { background:#fee2e2; }
|
|
||||||
.grid { display:grid; grid-template-columns: 1fr; gap: 1rem; }
|
|
||||||
@media(min-width: 900px){ .grid{ grid-template-columns: 1fr 1fr; } }
|
|
||||||
.card { border:1px solid #eee; border-radius:.75rem; padding:1rem; background:#fff; box-shadow:0 1px 4px rgba(0,0,0,.04); }
|
|
||||||
table { width:100%; border-collapse:collapse; }
|
|
||||||
th, td { border-bottom:1px solid #eee; padding:.4rem .5rem; }
|
|
||||||
td.num { text-align:right; font-variant-numeric: tabular-nums; }
|
|
||||||
button { cursor:pointer; }
|
|
||||||
.muted{ color:#666; }
|
|
||||||
.now { display:flex; gap:.5rem; align-items:center; }
|
|
||||||
progress { width: 220px; height: 10px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="toolbar card">
|
<header>
|
||||||
<span>Loop:</span>
|
<h1>Trading Dashboard</h1>
|
||||||
<span id="loopState" class="badge">–</span>
|
<div class="meta">
|
||||||
<button id="btnStart">Start</button>
|
<span id="last-update">Ostatnia aktualizacja: —</span>
|
||||||
<button id="btnStop">Stop</button>
|
<button id="refresh-btn">Odśwież teraz</button>
|
||||||
<button id="btnTick" title="Wykonaj jedną rundę (1 ticker)">Tick</button>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<span>| Auto-refresh:</span>
|
<main>
|
||||||
<button id="autoOn">On</button>
|
<!-- KARTY -->
|
||||||
<button id="autoOff">Off</button>
|
<section class="cards">
|
||||||
<label class="muted">co <select id="refreshMs">
|
<div class="card">
|
||||||
<option value="1000">1s</option>
|
<div class="card-title">Gotówka</div>
|
||||||
<option value="2000" selected>2s</option>
|
<div class="card-value" id="cash">—</div>
|
||||||
<option value="5000">5s</option>
|
</div>
|
||||||
<option value="10000">10s</option>
|
<div class="card">
|
||||||
</select></label>
|
<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>
|
<!-- WYKRES WARTOŚCI KONTA (TOTAL) -->
|
||||||
<span>| Gotówka: <b id="cash">–</b></span>
|
<section>
|
||||||
</div>
|
<h2>Wartość konta (total) — wykres</h2>
|
||||||
|
<div class="chart-wrap">
|
||||||
|
<canvas id="totalChart" width="1200" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="card now">
|
<!-- WYKRES GOTÓWKI -->
|
||||||
<b>Teraz:</b>
|
<section>
|
||||||
<span id="stage">–</span>
|
<h2>Gotówka — wykres</h2>
|
||||||
<span id="ticker">–</span>
|
<div class="chart-wrap">
|
||||||
<span id="progressText" class="muted">0 / 0</span>
|
<canvas id="cashChart" width="1200" height="300"></canvas>
|
||||||
<progress id="progress" value="0" max="0"></progress>
|
</div>
|
||||||
<span class="muted">Ostatnia akcja:</span>
|
</section>
|
||||||
<span id="lastAction">–</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid">
|
<!-- Otwarte pozycje -->
|
||||||
<div class="card">
|
<section>
|
||||||
<h3>Pozycje</h3>
|
<h2>Otwarte pozycje</h2>
|
||||||
<table id="positions">
|
<table id="positions-table">
|
||||||
<thead>
|
<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>
|
</thead>
|
||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</section>
|
||||||
<div class="card">
|
|
||||||
<h3>Transakcje (ostatnie 50)</h3>
|
<!-- Ostatnie sygnały -->
|
||||||
<table id="trades">
|
<section>
|
||||||
|
<h2>Ostatnie sygnały</h2>
|
||||||
|
<table id="signals-table">
|
||||||
<thead>
|
<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>
|
</thead>
|
||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
</body>
|
||||||
</html>
|
</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