Use Kelly allocation for Torch position scaling
This commit is contained in:
@@ -33,11 +33,11 @@ LEARNING_MIN_SAMPLES=3
|
|||||||
LEARNING_MAX_ADJUSTMENT=0.12
|
LEARNING_MAX_ADJUSTMENT=0.12
|
||||||
LEARNING_MAX_POSITION_MULTIPLIER=1.6
|
LEARNING_MAX_POSITION_MULTIPLIER=1.6
|
||||||
MIN_POSITION_USDT=1
|
MIN_POSITION_USDT=1
|
||||||
MAX_POSITION_USDT=25
|
MAX_POSITION_USDT=8
|
||||||
MAX_SYMBOL_EXPOSURE_USDT=25
|
MAX_SYMBOL_EXPOSURE_USDT=25
|
||||||
MAX_TOTAL_EXPOSURE_USDT=75
|
MAX_TOTAL_EXPOSURE_USDT=75
|
||||||
MAX_OPEN_POSITIONS=4
|
MAX_OPEN_POSITIONS=24
|
||||||
MAX_POSITIONS_PER_SYMBOL=1
|
MAX_POSITIONS_PER_SYMBOL=6
|
||||||
GRID_TRADING_ENABLED=false
|
GRID_TRADING_ENABLED=false
|
||||||
GRID_ENTRY_CONFIDENCE=0.58
|
GRID_ENTRY_CONFIDENCE=0.58
|
||||||
GRID_BUY_ZONE=0.45
|
GRID_BUY_ZONE=0.45
|
||||||
@@ -46,7 +46,7 @@ REBOUND_TRADING_ENABLED=true
|
|||||||
REBOUND_ENTRY_CONFIDENCE=0.55
|
REBOUND_ENTRY_CONFIDENCE=0.55
|
||||||
REBOUND_MIN_PROBABILITY=0.55
|
REBOUND_MIN_PROBABILITY=0.55
|
||||||
REBOUND_MAX_POSITION_USDT=6
|
REBOUND_MAX_POSITION_USDT=6
|
||||||
KELLY_SIZING_ENABLED=false
|
KELLY_SIZING_ENABLED=true
|
||||||
KELLY_FRACTION=0.25
|
KELLY_FRACTION=0.25
|
||||||
KELLY_MAX_FRACTION=0.20
|
KELLY_MAX_FRACTION=0.20
|
||||||
RISK_PER_TRADE_PERCENT=0.01
|
RISK_PER_TRADE_PERCENT=0.01
|
||||||
|
|||||||
+6
-6
@@ -33,11 +33,11 @@ LEARNING_MIN_SAMPLES=3
|
|||||||
LEARNING_MAX_ADJUSTMENT=0.12
|
LEARNING_MAX_ADJUSTMENT=0.12
|
||||||
LEARNING_MAX_POSITION_MULTIPLIER=1.6
|
LEARNING_MAX_POSITION_MULTIPLIER=1.6
|
||||||
MIN_POSITION_USDT=1
|
MIN_POSITION_USDT=1
|
||||||
MAX_POSITION_USDT=25
|
MAX_POSITION_USDT=8
|
||||||
MAX_SYMBOL_EXPOSURE_USDT=25
|
MAX_SYMBOL_EXPOSURE_USDT=25
|
||||||
MAX_TOTAL_EXPOSURE_USDT=75
|
MAX_TOTAL_EXPOSURE_USDT=75
|
||||||
MAX_OPEN_POSITIONS=4
|
MAX_OPEN_POSITIONS=24
|
||||||
MAX_POSITIONS_PER_SYMBOL=1
|
MAX_POSITIONS_PER_SYMBOL=6
|
||||||
GRID_TRADING_ENABLED=false
|
GRID_TRADING_ENABLED=false
|
||||||
GRID_ENTRY_CONFIDENCE=0.58
|
GRID_ENTRY_CONFIDENCE=0.58
|
||||||
GRID_BUY_ZONE=0.45
|
GRID_BUY_ZONE=0.45
|
||||||
@@ -46,7 +46,7 @@ REBOUND_TRADING_ENABLED=true
|
|||||||
REBOUND_ENTRY_CONFIDENCE=0.55
|
REBOUND_ENTRY_CONFIDENCE=0.55
|
||||||
REBOUND_MIN_PROBABILITY=0.55
|
REBOUND_MIN_PROBABILITY=0.55
|
||||||
REBOUND_MAX_POSITION_USDT=6
|
REBOUND_MAX_POSITION_USDT=6
|
||||||
KELLY_SIZING_ENABLED=false
|
KELLY_SIZING_ENABLED=true
|
||||||
KELLY_FRACTION=0.25
|
KELLY_FRACTION=0.25
|
||||||
KELLY_MAX_FRACTION=0.20
|
KELLY_MAX_FRACTION=0.20
|
||||||
RISK_PER_TRADE_PERCENT=0.01
|
RISK_PER_TRADE_PERCENT=0.01
|
||||||
@@ -61,8 +61,8 @@ TREND_RSI_MAX=65
|
|||||||
TIME_SERIES_FORECAST_ENABLED=true
|
TIME_SERIES_FORECAST_ENABLED=true
|
||||||
TIME_SERIES_MIN_CANDLES=120
|
TIME_SERIES_MIN_CANDLES=120
|
||||||
TIME_SERIES_FORECAST_HORIZON=3
|
TIME_SERIES_FORECAST_HORIZON=3
|
||||||
TIME_SERIES_MIN_EDGE_PERCENT=0.08
|
TIME_SERIES_MIN_EDGE_PERCENT=0.10
|
||||||
TIME_SERIES_MIN_PROBABILITY_UP=0.58
|
TIME_SERIES_MIN_PROBABILITY_UP=0.47
|
||||||
TIME_SERIES_MIN_CONFIDENCE=0.4
|
TIME_SERIES_MIN_CONFIDENCE=0.4
|
||||||
TIME_SERIES_MAX_ADJUSTMENT=0.08
|
TIME_SERIES_MAX_ADJUSTMENT=0.08
|
||||||
TIME_SERIES_LSTM_ENABLED=true
|
TIME_SERIES_LSTM_ENABLED=true
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Spot-бот для демо-торговли криптовалютой на р
|
|||||||
## Что реализовано
|
## Что реализовано
|
||||||
|
|
||||||
- Реальные market data Bybit Spot: REST bootstrap и WebSocket-обновления.
|
- Реальные market data Bybit Spot: REST bootstrap и WebSocket-обновления.
|
||||||
- Фиксированный набор USDT spot-пар для основной стратегии: `BTCUSDT`, `ETHUSDT`, `SOLUSDT`, `LTCUSDT`.
|
- Фиксированный набор 12 USDT spot-пар для основной стратегии: `BTCUSDT`, `ETHUSDT`, `HYPEUSDT`, `SOLUSDT`, `XRPUSDT`, `XPLUSDT`, `WLDUSDT`, `MNTUSDT`, `HUSDT`, `XAUTUSDT`, `IPUSDT`, `AAVEUSDT`.
|
||||||
- Paper trading с учетом cash, комиссий, проскальзывания, stop-loss, take-profit и trailing stop.
|
- Paper trading с учетом cash, комиссий, проскальзывания, stop-loss, take-profit и trailing stop.
|
||||||
- Spot-only логика: покупка базовой монеты за USDT и продажа обратно, без short и без плеча.
|
- Spot-only логика: покупка базовой монеты за USDT и продажа обратно, без short и без плеча.
|
||||||
- Live spot-ордеры явно отправляются без плеча: `category=spot`, `isLeverage=0`.
|
- Live spot-ордеры явно отправляются без плеча: `category=spot`, `isLeverage=0`.
|
||||||
@@ -88,7 +88,7 @@ powershell -ExecutionPolicy Bypass -File tools\run_torch_retrain.ps1
|
|||||||
powershell -ExecutionPolicy Bypass -File tools\install_windows_torch_retrainer.ps1
|
powershell -ExecutionPolicy Bypass -File tools\install_windows_torch_retrainer.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
По умолчанию Windows-расписание переобучает PyTorch `LSTM/GRU` каждые 6 часов с `--limit 3000` на парах `BTCUSDT,ETHUSDT,SOLUSDT,LTCUSDT`. Параметры можно переопределить через env: `TORCH_RETRAIN_SYMBOLS`, `TORCH_RETRAIN_LIMIT`, `TORCH_RETRAIN_LOOKBACKS`, `TORCH_RETRAIN_ARCHITECTURES`, `TORCH_RETRAIN_HIDDEN_SIZES`, `TORCH_RETRAIN_LAYERS`, `TORCH_RETRAIN_DROPOUTS`, `TORCH_RETRAIN_HORIZON`, `TORCH_RETRAIN_HORIZONS`, `TORCH_RETRAIN_CONTEXT_SYMBOLS`, `TORCH_RETRAIN_FEATURES`, `TORCH_RETRAIN_SEED`, `TORCH_RETRAIN_EPOCHS`, `TORCH_RETRAIN_PATIENCE`, `TORCH_RETRAIN_INTERVAL`, `TORCH_RETRAIN_ENV`.
|
По умолчанию Windows-расписание переобучает PyTorch `LSTM/GRU` каждые 6 часов с `--limit 3000` на 12 spot-парах из `SYMBOLS`. Параметры можно переопределить через env: `TORCH_RETRAIN_SYMBOLS`, `TORCH_RETRAIN_LIMIT`, `TORCH_RETRAIN_LOOKBACKS`, `TORCH_RETRAIN_ARCHITECTURES`, `TORCH_RETRAIN_HIDDEN_SIZES`, `TORCH_RETRAIN_LAYERS`, `TORCH_RETRAIN_DROPOUTS`, `TORCH_RETRAIN_HORIZON`, `TORCH_RETRAIN_HORIZONS`, `TORCH_RETRAIN_CONTEXT_SYMBOLS`, `TORCH_RETRAIN_FEATURES`, `TORCH_RETRAIN_SEED`, `TORCH_RETRAIN_EPOCHS`, `TORCH_RETRAIN_PATIENCE`, `TORCH_RETRAIN_INTERVAL`, `TORCH_RETRAIN_ENV`.
|
||||||
|
|
||||||
Если retrain запускается с `-DeployToPi`, после успешного guard он синхронизирует `runtime/lstm_forecaster.json`, `runtime/torch_retrain_guard.json` и `runtime/torch_threshold_calibration.json` на Raspberry Pi через SSH-ключ и перезапускает сервис `tradebot`. Отдельный запуск sync:
|
Если retrain запускается с `-DeployToPi`, после успешного guard он синхронизирует `runtime/lstm_forecaster.json`, `runtime/torch_retrain_guard.json` и `runtime/torch_threshold_calibration.json` на Raspberry Pi через SSH-ключ и перезапускает сервис `tradebot`. Отдельный запуск sync:
|
||||||
|
|
||||||
@@ -137,11 +137,11 @@ LEARNING_MIN_SAMPLES=3
|
|||||||
LEARNING_MAX_ADJUSTMENT=0.12
|
LEARNING_MAX_ADJUSTMENT=0.12
|
||||||
LEARNING_MAX_POSITION_MULTIPLIER=1.6
|
LEARNING_MAX_POSITION_MULTIPLIER=1.6
|
||||||
MIN_POSITION_USDT=1
|
MIN_POSITION_USDT=1
|
||||||
MAX_POSITION_USDT=25
|
MAX_POSITION_USDT=8
|
||||||
MAX_SYMBOL_EXPOSURE_USDT=25
|
MAX_SYMBOL_EXPOSURE_USDT=25
|
||||||
MAX_TOTAL_EXPOSURE_USDT=75
|
MAX_TOTAL_EXPOSURE_USDT=75
|
||||||
MAX_OPEN_POSITIONS=4
|
MAX_OPEN_POSITIONS=24
|
||||||
MAX_POSITIONS_PER_SYMBOL=1
|
MAX_POSITIONS_PER_SYMBOL=6
|
||||||
GRID_TRADING_ENABLED=false
|
GRID_TRADING_ENABLED=false
|
||||||
GRID_ENTRY_CONFIDENCE=0.58
|
GRID_ENTRY_CONFIDENCE=0.58
|
||||||
GRID_BUY_ZONE=0.45
|
GRID_BUY_ZONE=0.45
|
||||||
@@ -150,7 +150,7 @@ REBOUND_TRADING_ENABLED=true
|
|||||||
REBOUND_ENTRY_CONFIDENCE=0.55
|
REBOUND_ENTRY_CONFIDENCE=0.55
|
||||||
REBOUND_MIN_PROBABILITY=0.55
|
REBOUND_MIN_PROBABILITY=0.55
|
||||||
REBOUND_MAX_POSITION_USDT=6
|
REBOUND_MAX_POSITION_USDT=6
|
||||||
KELLY_SIZING_ENABLED=false
|
KELLY_SIZING_ENABLED=true
|
||||||
KELLY_FRACTION=0.25
|
KELLY_FRACTION=0.25
|
||||||
KELLY_MAX_FRACTION=0.20
|
KELLY_MAX_FRACTION=0.20
|
||||||
RISK_PER_TRADE_PERCENT=0.01
|
RISK_PER_TRADE_PERCENT=0.01
|
||||||
@@ -165,8 +165,8 @@ TREND_RSI_MAX=65
|
|||||||
TIME_SERIES_FORECAST_ENABLED=true
|
TIME_SERIES_FORECAST_ENABLED=true
|
||||||
TIME_SERIES_MIN_CANDLES=120
|
TIME_SERIES_MIN_CANDLES=120
|
||||||
TIME_SERIES_FORECAST_HORIZON=3
|
TIME_SERIES_FORECAST_HORIZON=3
|
||||||
TIME_SERIES_MIN_EDGE_PERCENT=0.08
|
TIME_SERIES_MIN_EDGE_PERCENT=0.10
|
||||||
TIME_SERIES_MIN_PROBABILITY_UP=0.58
|
TIME_SERIES_MIN_PROBABILITY_UP=0.47
|
||||||
TIME_SERIES_MIN_CONFIDENCE=0.4
|
TIME_SERIES_MIN_CONFIDENCE=0.4
|
||||||
TIME_SERIES_MAX_ADJUSTMENT=0.08
|
TIME_SERIES_MAX_ADJUSTMENT=0.08
|
||||||
TIME_SERIES_LSTM_ENABLED=true
|
TIME_SERIES_LSTM_ENABLED=true
|
||||||
|
|||||||
@@ -156,6 +156,9 @@ class CryptoSpotBot:
|
|||||||
learning["adaptive_rules"] = self._with_exposure_context(learning.get("adaptive_rules") or {})
|
learning["adaptive_rules"] = self._with_exposure_context(learning.get("adaptive_rules") or {})
|
||||||
account = self.broker.account_state(prices)
|
account = self.broker.account_state(prices)
|
||||||
account["risk_guard"] = risk_guard
|
account["risk_guard"] = risk_guard
|
||||||
|
account["symbol"] = symbol
|
||||||
|
account["symbol_exposure_usdt"] = self.broker.symbol_exposure(symbol)
|
||||||
|
account["open_positions_for_symbol"] = open_count
|
||||||
if risk_guard.get("block_new_entries"):
|
if risk_guard.get("block_new_entries"):
|
||||||
self.storage.insert_signal(
|
self.storage.insert_signal(
|
||||||
Signal(
|
Signal(
|
||||||
|
|||||||
@@ -5,7 +5,20 @@ from dataclasses import dataclass
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
FIXED_SPOT_SYMBOLS = ("BTCUSDT", "ETHUSDT", "SOLUSDT", "LTCUSDT")
|
FIXED_SPOT_SYMBOLS = (
|
||||||
|
"BTCUSDT",
|
||||||
|
"ETHUSDT",
|
||||||
|
"HYPEUSDT",
|
||||||
|
"SOLUSDT",
|
||||||
|
"XRPUSDT",
|
||||||
|
"XPLUSDT",
|
||||||
|
"WLDUSDT",
|
||||||
|
"MNTUSDT",
|
||||||
|
"HUSDT",
|
||||||
|
"XAUTUSDT",
|
||||||
|
"IPUSDT",
|
||||||
|
"AAVEUSDT",
|
||||||
|
)
|
||||||
STRATEGY_MODES = {"legacy", "trend_macd", "torch_forecast"}
|
STRATEGY_MODES = {"legacy", "trend_macd", "torch_forecast"}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -94,10 +94,7 @@ class PaperBroker:
|
|||||||
return False, "достигнут общий лимит открытых позиций"
|
return False, "достигнут общий лимит открытых позиций"
|
||||||
if self.settings.strategy_mode == "trend_macd" and len(self.positions_for_symbol(symbol)) >= 1:
|
if self.settings.strategy_mode == "trend_macd" and len(self.positions_for_symbol(symbol)) >= 1:
|
||||||
return False, "DCA/усреднение отключено: позиция по паре уже открыта"
|
return False, "DCA/усреднение отключено: позиция по паре уже открыта"
|
||||||
dynamic_pair_limit = max(
|
dynamic_pair_limit = _symbol_position_limit(self.settings)
|
||||||
self.settings.max_positions_per_symbol,
|
|
||||||
int(self.settings.max_symbol_exposure_usdt // max(self.settings.min_position_usdt, 0.01)),
|
|
||||||
)
|
|
||||||
if len(self.positions_for_symbol(symbol)) >= dynamic_pair_limit:
|
if len(self.positions_for_symbol(symbol)) >= dynamic_pair_limit:
|
||||||
return False, "достигнут лимит позиций по паре"
|
return False, "достигнут лимит позиций по паре"
|
||||||
requested = requested_notional if requested_notional is not None else self.settings.min_position_usdt
|
requested = requested_notional if requested_notional is not None else self.settings.min_position_usdt
|
||||||
@@ -384,3 +381,12 @@ class LiveBroker(PaperBroker):
|
|||||||
|
|
||||||
def prices_from_tickers(tickers: Iterable[Ticker]) -> dict[str, float]:
|
def prices_from_tickers(tickers: Iterable[Ticker]) -> dict[str, float]:
|
||||||
return {ticker.symbol: ticker.last_price for ticker in tickers}
|
return {ticker.symbol: ticker.last_price for ticker in tickers}
|
||||||
|
|
||||||
|
|
||||||
|
def _symbol_position_limit(settings: Settings) -> int:
|
||||||
|
configured_limit = max(1, settings.max_positions_per_symbol)
|
||||||
|
exposure_based_limit = max(
|
||||||
|
1,
|
||||||
|
int(settings.max_symbol_exposure_usdt // max(settings.min_position_usdt, 0.01)),
|
||||||
|
)
|
||||||
|
return min(configured_limit, exposure_based_limit)
|
||||||
|
|||||||
@@ -16,7 +16,20 @@ from crypto_spot_bot.models import Candle, Ticker, utc_now
|
|||||||
from crypto_spot_bot.storage import Storage
|
from crypto_spot_bot.storage import Storage
|
||||||
|
|
||||||
|
|
||||||
POPULAR_FALLBACK = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "LTCUSDT"]
|
POPULAR_FALLBACK = [
|
||||||
|
"BTCUSDT",
|
||||||
|
"ETHUSDT",
|
||||||
|
"HYPEUSDT",
|
||||||
|
"SOLUSDT",
|
||||||
|
"XRPUSDT",
|
||||||
|
"XPLUSDT",
|
||||||
|
"WLDUSDT",
|
||||||
|
"MNTUSDT",
|
||||||
|
"HUSDT",
|
||||||
|
"XAUTUSDT",
|
||||||
|
"IPUSDT",
|
||||||
|
"AAVEUSDT",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _float(value: Any, default: float = 0.0) -> float:
|
def _float(value: Any, default: float = 0.0) -> float:
|
||||||
|
|||||||
+67
-19
@@ -632,7 +632,7 @@ def _torch_forecast_entry_signal(
|
|||||||
return Signal(symbol, "HOLD", 0.0, "torch_forecast: symbol position limit reached")
|
return Signal(symbol, "HOLD", 0.0, "torch_forecast: symbol position limit reached")
|
||||||
|
|
||||||
stop_loss_percent = _clamp(settings.stop_loss_percent, 0.003, 0.08)
|
stop_loss_percent = _clamp(settings.stop_loss_percent, 0.003, 0.08)
|
||||||
sizing = _torch_forecast_position_sizing(settings, account, stop_loss_percent, forecast)
|
sizing = _torch_forecast_position_sizing(settings, account, stop_loss_percent, forecast, symbol)
|
||||||
position_notional = float(sizing["notional_usdt"])
|
position_notional = float(sizing["notional_usdt"])
|
||||||
expected_return = _safe_float(forecast.get("expected_return_percent"), 0.0)
|
expected_return = _safe_float(forecast.get("expected_return_percent"), 0.0)
|
||||||
probability_up = _safe_float(forecast.get("probability_up"), 0.5)
|
probability_up = _safe_float(forecast.get("probability_up"), 0.5)
|
||||||
@@ -721,6 +721,8 @@ def _torch_forecast_entry_signal(
|
|||||||
"rebound_probability": rebound.get("probability", 0.0),
|
"rebound_probability": rebound.get("probability", 0.0),
|
||||||
}
|
}
|
||||||
edge_mode = "rebound_fallback" if fallback_rebound_entry_ok else "rebound"
|
edge_mode = "rebound_fallback" if fallback_rebound_entry_ok else "rebound"
|
||||||
|
risk_size_ok = position_notional >= settings.min_position_usdt
|
||||||
|
rebound_entry_sized_ok = rebound_entry_ok and risk_size_ok
|
||||||
checks = {
|
checks = {
|
||||||
"torch_model_ok": model_ok,
|
"torch_model_ok": model_ok,
|
||||||
"quality_gate_ok": quality_gate_ok,
|
"quality_gate_ok": quality_gate_ok,
|
||||||
@@ -732,7 +734,7 @@ def _torch_forecast_entry_signal(
|
|||||||
"confidence_ok": confidence >= settings.time_series_min_confidence,
|
"confidence_ok": confidence >= settings.time_series_min_confidence,
|
||||||
"spread_ok": spread_ok,
|
"spread_ok": spread_ok,
|
||||||
"liquidity_ok": liquidity_ok,
|
"liquidity_ok": liquidity_ok,
|
||||||
"risk_size_ok": position_notional >= settings.min_position_usdt,
|
"risk_size_ok": risk_size_ok,
|
||||||
}
|
}
|
||||||
diagnostics = {
|
diagnostics = {
|
||||||
"strategy_mode": "torch_forecast",
|
"strategy_mode": "torch_forecast",
|
||||||
@@ -756,6 +758,7 @@ def _torch_forecast_entry_signal(
|
|||||||
"model_rebound_entry_ok": model_rebound_entry_ok,
|
"model_rebound_entry_ok": model_rebound_entry_ok,
|
||||||
"fallback_rebound_entry_ok": fallback_rebound_entry_ok,
|
"fallback_rebound_entry_ok": fallback_rebound_entry_ok,
|
||||||
"rebound_entry_ok": rebound_entry_ok,
|
"rebound_entry_ok": rebound_entry_ok,
|
||||||
|
"rebound_entry_sized_ok": rebound_entry_sized_ok,
|
||||||
"min_confidence": settings.time_series_min_confidence,
|
"min_confidence": settings.time_series_min_confidence,
|
||||||
"skill": skill,
|
"skill": skill,
|
||||||
"quality_gate": forecast.get("quality_gate", {}),
|
"quality_gate": forecast.get("quality_gate", {}),
|
||||||
@@ -769,8 +772,8 @@ def _torch_forecast_entry_signal(
|
|||||||
"llm": {},
|
"llm": {},
|
||||||
}
|
}
|
||||||
base_entry_ok = all(checks.values())
|
base_entry_ok = all(checks.values())
|
||||||
if base_entry_ok or rebound_entry_ok:
|
if base_entry_ok or rebound_entry_sized_ok:
|
||||||
buy_confidence = max(confidence, float(rebound.get("probability", 0.0) or 0.0)) if rebound_entry_ok else confidence
|
buy_confidence = max(confidence, float(rebound.get("probability", 0.0) or 0.0)) if rebound_entry_sized_ok else confidence
|
||||||
entry_path = edge_mode if rebound_entry_ok and not base_entry_ok else edge_mode
|
entry_path = edge_mode if rebound_entry_ok and not base_entry_ok else edge_mode
|
||||||
diagnostics["entry_path"] = entry_path
|
diagnostics["entry_path"] = entry_path
|
||||||
if fallback_rebound_entry_ok and not base_entry_ok:
|
if fallback_rebound_entry_ok and not base_entry_ok:
|
||||||
@@ -961,8 +964,12 @@ def _torch_min_probability(settings: Settings) -> float:
|
|||||||
|
|
||||||
|
|
||||||
def _dynamic_symbol_position_limit(settings: Settings) -> int:
|
def _dynamic_symbol_position_limit(settings: Settings) -> int:
|
||||||
exposure_based_limit = int(settings.max_symbol_exposure_usdt // max(settings.min_position_usdt, 0.01))
|
configured_limit = max(1, settings.max_positions_per_symbol)
|
||||||
return max(1, settings.max_positions_per_symbol, exposure_based_limit)
|
exposure_based_limit = max(
|
||||||
|
1,
|
||||||
|
int(settings.max_symbol_exposure_usdt // max(settings.min_position_usdt, 0.01)),
|
||||||
|
)
|
||||||
|
return min(configured_limit, exposure_based_limit)
|
||||||
|
|
||||||
|
|
||||||
def _torch_forecast_confidence(settings: Settings, forecast: dict) -> float:
|
def _torch_forecast_confidence(settings: Settings, forecast: dict) -> float:
|
||||||
@@ -982,30 +989,43 @@ def _torch_forecast_position_sizing(
|
|||||||
account: dict | None,
|
account: dict | None,
|
||||||
stop_loss_percent: float,
|
stop_loss_percent: float,
|
||||||
forecast: dict,
|
forecast: dict,
|
||||||
|
symbol: str | None = None,
|
||||||
) -> dict[str, float | str]:
|
) -> dict[str, float | str]:
|
||||||
base = _trend_position_sizing(settings, account, stop_loss_percent)
|
base = _trend_position_sizing(settings, account, stop_loss_percent)
|
||||||
base_notional = float(base["notional_usdt"])
|
base_notional = float(base["notional_usdt"])
|
||||||
if base_notional <= 0:
|
kelly = _kelly_position(
|
||||||
|
settings=settings,
|
||||||
|
final_score=_torch_forecast_confidence(settings, forecast),
|
||||||
|
forecast=forecast,
|
||||||
|
adaptive={},
|
||||||
|
account=account,
|
||||||
|
symbol=symbol,
|
||||||
|
)
|
||||||
|
expected_return = max(0.0, _safe_float(forecast.get("expected_return_percent"), 0.0))
|
||||||
|
probability_up = _safe_float(forecast.get("probability_up"), 0.5)
|
||||||
|
skill = max(0.0, _safe_float(forecast.get("skill"), 0.0))
|
||||||
|
min_edge = max(0.01, settings.time_series_min_edge_percent)
|
||||||
|
edge_multiplier = _clamp(expected_return / max(min_edge * 3.0, 0.01), 0.25, 1.15)
|
||||||
|
probability_multiplier = _clamp(0.75 + (probability_up - 0.55) * 3.0, 0.50, 1.20)
|
||||||
|
skill_multiplier = _clamp(0.85 + skill * 0.60, 0.60, 1.15)
|
||||||
|
if settings.kelly_sizing_enabled:
|
||||||
|
raw = float(kelly["kelly_remaining_notional_usdt"]) * _risk_guard_multiplier(account)
|
||||||
|
notional = 0.0 if raw < settings.min_position_usdt else min(raw, settings.max_position_usdt)
|
||||||
|
elif base_notional <= 0:
|
||||||
notional = 0.0
|
notional = 0.0
|
||||||
edge_multiplier = probability_multiplier = skill_multiplier = 0.0
|
|
||||||
else:
|
else:
|
||||||
expected_return = max(0.0, _safe_float(forecast.get("expected_return_percent"), 0.0))
|
|
||||||
probability_up = _safe_float(forecast.get("probability_up"), 0.5)
|
|
||||||
skill = max(0.0, _safe_float(forecast.get("skill"), 0.0))
|
|
||||||
min_edge = max(0.01, settings.time_series_min_edge_percent)
|
|
||||||
edge_multiplier = _clamp(expected_return / max(min_edge * 3.0, 0.01), 0.25, 1.15)
|
|
||||||
probability_multiplier = _clamp(0.75 + (probability_up - 0.55) * 3.0, 0.50, 1.20)
|
|
||||||
skill_multiplier = _clamp(0.85 + skill * 0.60, 0.60, 1.15)
|
|
||||||
raw = base_notional * edge_multiplier * probability_multiplier * skill_multiplier
|
raw = base_notional * edge_multiplier * probability_multiplier * skill_multiplier
|
||||||
notional = 0.0 if raw < settings.min_position_usdt else min(raw, settings.max_position_usdt)
|
notional = 0.0 if raw < settings.min_position_usdt else min(raw, settings.max_position_usdt)
|
||||||
return {
|
return {
|
||||||
**base,
|
**base,
|
||||||
"method": "torch_forecast_risk",
|
"method": "torch_forecast_fractional_kelly" if settings.kelly_sizing_enabled else "torch_forecast_risk",
|
||||||
|
"enabled": bool(settings.kelly_sizing_enabled),
|
||||||
"notional_usdt": round(notional, 2),
|
"notional_usdt": round(notional, 2),
|
||||||
"base_notional_usdt": base["notional_usdt"],
|
"base_notional_usdt": base["notional_usdt"],
|
||||||
"torch_edge_multiplier": round(edge_multiplier, 4),
|
"torch_edge_multiplier": round(edge_multiplier, 4),
|
||||||
"torch_probability_multiplier": round(probability_multiplier, 4),
|
"torch_probability_multiplier": round(probability_multiplier, 4),
|
||||||
"torch_skill_multiplier": round(skill_multiplier, 4),
|
"torch_skill_multiplier": round(skill_multiplier, 4),
|
||||||
|
**kelly,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1168,6 +1188,7 @@ def _kelly_position(
|
|||||||
forecast: dict,
|
forecast: dict,
|
||||||
adaptive: dict,
|
adaptive: dict,
|
||||||
account: dict | None,
|
account: dict | None,
|
||||||
|
symbol: str | None = None,
|
||||||
) -> dict[str, float | bool | str]:
|
) -> dict[str, float | bool | str]:
|
||||||
confidence_probability = _confidence_probability(final_score, settings.min_signal_confidence)
|
confidence_probability = _confidence_probability(final_score, settings.min_signal_confidence)
|
||||||
probability_source = "confidence"
|
probability_source = "confidence"
|
||||||
@@ -1180,8 +1201,13 @@ def _kelly_position(
|
|||||||
stop_loss = _adaptive_percent(adaptive, "stop_loss_percent", settings.stop_loss_percent, 0.003, 0.08)
|
stop_loss = _adaptive_percent(adaptive, "stop_loss_percent", settings.stop_loss_percent, 0.003, 0.08)
|
||||||
take_profit = _adaptive_percent(adaptive, "take_profit_percent", settings.take_profit_percent, 0.003, 0.20)
|
take_profit = _adaptive_percent(adaptive, "take_profit_percent", settings.take_profit_percent, 0.003, 0.20)
|
||||||
round_trip_cost = max(0.0, 2.0 * (settings.taker_fee_rate + settings.slippage_rate))
|
round_trip_cost = max(0.0, 2.0 * (settings.taker_fee_rate + settings.slippage_rate))
|
||||||
win_return = max(0.0, take_profit - round_trip_cost)
|
base_win_return = max(0.0, take_profit - round_trip_cost)
|
||||||
loss_return = max(0.0001, stop_loss + round_trip_cost)
|
loss_return = max(0.0001, stop_loss + round_trip_cost)
|
||||||
|
expected_net_return = max(0.0, _safe_float(forecast.get("expected_return_percent"), 0.0) / 100.0)
|
||||||
|
implied_win_return = 0.0
|
||||||
|
if probability > 0:
|
||||||
|
implied_win_return = max(0.0, (expected_net_return + (1.0 - probability) * loss_return) / probability)
|
||||||
|
win_return = max(base_win_return, implied_win_return)
|
||||||
reward_loss_ratio = win_return / loss_return if loss_return > 0 else 0.0
|
reward_loss_ratio = win_return / loss_return if loss_return > 0 else 0.0
|
||||||
full_kelly = probability - ((1.0 - probability) / reward_loss_ratio) if reward_loss_ratio > 0 else 0.0
|
full_kelly = probability - ((1.0 - probability) / reward_loss_ratio) if reward_loss_ratio > 0 else 0.0
|
||||||
full_kelly = max(0.0, full_kelly)
|
full_kelly = max(0.0, full_kelly)
|
||||||
@@ -1190,19 +1216,41 @@ def _kelly_position(
|
|||||||
bankroll = _safe_float((account or {}).get("equity"), settings.starting_balance_usdt)
|
bankroll = _safe_float((account or {}).get("equity"), settings.starting_balance_usdt)
|
||||||
if bankroll <= 0:
|
if bankroll <= 0:
|
||||||
bankroll = settings.starting_balance_usdt
|
bankroll = settings.starting_balance_usdt
|
||||||
kelly_notional = max(0.0, bankroll * effective_fraction)
|
target_notional = max(0.0, bankroll * effective_fraction)
|
||||||
|
open_symbol_exposure = _account_symbol_exposure(account, symbol)
|
||||||
|
remaining_notional = max(0.0, target_notional - open_symbol_exposure)
|
||||||
return {
|
return {
|
||||||
"kelly_probability": round(probability, 4),
|
"kelly_probability": round(probability, 4),
|
||||||
"kelly_probability_source": probability_source,
|
"kelly_probability_source": probability_source,
|
||||||
"kelly_reward_loss_ratio": round(reward_loss_ratio, 4),
|
"kelly_reward_loss_ratio": round(reward_loss_ratio, 4),
|
||||||
|
"kelly_win_return_percent": round(win_return * 100.0, 4),
|
||||||
|
"kelly_loss_return_percent": round(loss_return * 100.0, 4),
|
||||||
|
"kelly_expected_net_percent": round(expected_net_return * 100.0, 4),
|
||||||
"kelly_full_fraction": round(full_kelly, 4),
|
"kelly_full_fraction": round(full_kelly, 4),
|
||||||
"kelly_fractional_fraction": round(fractional_kelly, 4),
|
"kelly_fractional_fraction": round(fractional_kelly, 4),
|
||||||
"kelly_effective_fraction": round(effective_fraction, 4),
|
"kelly_effective_fraction": round(effective_fraction, 4),
|
||||||
"kelly_bankroll_usdt": round(bankroll, 2),
|
"kelly_bankroll_usdt": round(bankroll, 2),
|
||||||
"kelly_notional_usdt": round(kelly_notional, 2),
|
"kelly_target_notional_usdt": round(target_notional, 2),
|
||||||
|
"kelly_open_symbol_exposure_usdt": round(open_symbol_exposure, 2),
|
||||||
|
"kelly_remaining_notional_usdt": round(remaining_notional, 2),
|
||||||
|
"kelly_notional_usdt": round(remaining_notional, 2),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _account_symbol_exposure(account: dict | None, symbol: str | None = None) -> float:
|
||||||
|
if not isinstance(account, dict):
|
||||||
|
return 0.0
|
||||||
|
direct = _safe_float(account.get("symbol_exposure_usdt"), -1.0)
|
||||||
|
if direct >= 0:
|
||||||
|
return max(0.0, direct)
|
||||||
|
if not symbol:
|
||||||
|
symbol = str(account.get("symbol", "") or "")
|
||||||
|
exposures = account.get("symbol_exposures")
|
||||||
|
if isinstance(exposures, dict) and symbol:
|
||||||
|
return max(0.0, _safe_float(exposures.get(symbol), 0.0))
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
def _confidence_probability(final_score: float, min_signal_confidence: float) -> float:
|
def _confidence_probability(final_score: float, min_signal_confidence: float) -> float:
|
||||||
denominator = max(0.0001, 1.0 - min_signal_confidence)
|
denominator = max(0.0001, 1.0 - min_signal_confidence)
|
||||||
ratio = _clamp((final_score - min_signal_confidence) / denominator, 0.0, 1.0)
|
ratio = _clamp((final_score - min_signal_confidence) / denominator, 0.0, 1.0)
|
||||||
|
|||||||
+28
-1
@@ -60,7 +60,7 @@ def test_paper_broker_uses_signal_notional_and_pair_exposure(make_settings, tmp_
|
|||||||
max_symbol_exposure_usdt=6,
|
max_symbol_exposure_usdt=6,
|
||||||
max_total_exposure_usdt=50,
|
max_total_exposure_usdt=50,
|
||||||
max_open_positions=20,
|
max_open_positions=20,
|
||||||
max_positions_per_symbol=1,
|
max_positions_per_symbol=6,
|
||||||
max_entries_per_minute=0,
|
max_entries_per_minute=0,
|
||||||
)
|
)
|
||||||
storage = Storage(settings.database_path)
|
storage = Storage(settings.database_path)
|
||||||
@@ -218,6 +218,33 @@ def test_torch_forecast_broker_allows_dynamic_entries_until_total_limit(make_set
|
|||||||
assert len(broker.open_positions()) == 3
|
assert len(broker.open_positions()) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_torch_forecast_broker_respects_configured_symbol_position_limit(make_settings, tmp_path) -> None:
|
||||||
|
settings = make_settings(
|
||||||
|
tmp_path,
|
||||||
|
strategy_mode="torch_forecast",
|
||||||
|
min_position_usdt=1,
|
||||||
|
max_position_usdt=20,
|
||||||
|
max_symbol_exposure_usdt=20,
|
||||||
|
max_total_exposure_usdt=80,
|
||||||
|
max_open_positions=20,
|
||||||
|
max_positions_per_symbol=2,
|
||||||
|
max_entries_per_minute=0,
|
||||||
|
)
|
||||||
|
storage = Storage(settings.database_path)
|
||||||
|
broker = PaperBroker(settings, storage)
|
||||||
|
ticker = Ticker("BTCUSDT", 100, 99.9, 100.1, 10_000_000, 100, 0)
|
||||||
|
instrument = Instrument("BTCUSDT", "BTC", "USDT", "Trading", 0.01, 0.000001, 0.000001, 1)
|
||||||
|
|
||||||
|
first = broker.buy(Signal("BTCUSDT", "BUY", 0.8, "first", {"position_notional_usdt": 2}), ticker, instrument, {"BTCUSDT": 100})
|
||||||
|
second = broker.buy(Signal("BTCUSDT", "BUY", 0.8, "second", {"position_notional_usdt": 2}), ticker, instrument, {"BTCUSDT": 100})
|
||||||
|
third = broker.buy(Signal("BTCUSDT", "BUY", 0.8, "third", {"position_notional_usdt": 2}), ticker, instrument, {"BTCUSDT": 100})
|
||||||
|
|
||||||
|
assert first is not None
|
||||||
|
assert second is not None
|
||||||
|
assert third is None
|
||||||
|
assert len(broker.open_positions()) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_trend_macd_closes_old_paper_positions_outside_symbol_universe(make_settings, tmp_path) -> None:
|
def test_trend_macd_closes_old_paper_positions_outside_symbol_universe(make_settings, tmp_path) -> None:
|
||||||
settings = make_settings(
|
settings = make_settings(
|
||||||
tmp_path,
|
tmp_path,
|
||||||
|
|||||||
+80
-15
@@ -263,7 +263,8 @@ def test_torch_forecast_buys_only_from_positive_torch_edge(make_settings, tmp_pa
|
|||||||
assert signal.action == "BUY"
|
assert signal.action == "BUY"
|
||||||
assert signal.diagnostics["strategy_mode"] == "torch_forecast"
|
assert signal.diagnostics["strategy_mode"] == "torch_forecast"
|
||||||
assert signal.diagnostics["checks"]["torch_model_ok"] is True
|
assert signal.diagnostics["checks"]["torch_model_ok"] is True
|
||||||
assert signal.diagnostics["position_notional_usdt"] == 25.0
|
assert signal.diagnostics["position_sizing"]["method"] == "torch_forecast_fractional_kelly"
|
||||||
|
assert settings.min_position_usdt <= signal.diagnostics["position_notional_usdt"] <= settings.max_position_usdt
|
||||||
|
|
||||||
|
|
||||||
def test_torch_forecast_blocks_without_valid_torch_model(make_settings, tmp_path) -> None:
|
def test_torch_forecast_blocks_without_valid_torch_model(make_settings, tmp_path) -> None:
|
||||||
@@ -328,6 +329,71 @@ def test_torch_forecast_allows_additional_entries_until_symbol_limit(make_settin
|
|||||||
assert "symbol position limit" in capped.reason
|
assert "symbol position limit" in capped.reason
|
||||||
|
|
||||||
|
|
||||||
|
def test_torch_forecast_kelly_buys_only_remaining_symbol_allocation(make_settings, tmp_path) -> None:
|
||||||
|
settings = make_settings(
|
||||||
|
tmp_path,
|
||||||
|
strategy_mode="torch_forecast",
|
||||||
|
min_position_usdt=1,
|
||||||
|
max_position_usdt=8,
|
||||||
|
max_symbol_exposure_usdt=25,
|
||||||
|
max_positions_per_symbol=6,
|
||||||
|
stop_loss_percent=0.04,
|
||||||
|
take_profit_percent=0.035,
|
||||||
|
kelly_sizing_enabled=True,
|
||||||
|
kelly_fraction=0.25,
|
||||||
|
kelly_max_fraction=0.20,
|
||||||
|
time_series_min_edge_percent=0.10,
|
||||||
|
time_series_min_probability_up=0.47,
|
||||||
|
)
|
||||||
|
strategy = SpotStrategy(settings)
|
||||||
|
ticker = Ticker("BTCUSDT", 105, 104.99, 105.01, 10_000_000, 1000, 1.0)
|
||||||
|
forecast = {
|
||||||
|
"usable": True,
|
||||||
|
"model": "torch_gru",
|
||||||
|
"expected_return_percent": 0.60,
|
||||||
|
"probability_up": 0.84,
|
||||||
|
"skill": 0.22,
|
||||||
|
"block_entry": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
first = strategy.entry_signal(
|
||||||
|
"BTCUSDT",
|
||||||
|
[],
|
||||||
|
ticker,
|
||||||
|
open_positions_for_symbol=0,
|
||||||
|
forecast=forecast,
|
||||||
|
account={"equity": 100.0, "symbol": "BTCUSDT", "symbol_exposure_usdt": 0.0},
|
||||||
|
)
|
||||||
|
second = strategy.entry_signal(
|
||||||
|
"BTCUSDT",
|
||||||
|
[],
|
||||||
|
ticker,
|
||||||
|
open_positions_for_symbol=1,
|
||||||
|
forecast=forecast,
|
||||||
|
account={"equity": 100.0, "symbol": "BTCUSDT", "symbol_exposure_usdt": 8.0},
|
||||||
|
)
|
||||||
|
filled = strategy.entry_signal(
|
||||||
|
"BTCUSDT",
|
||||||
|
[],
|
||||||
|
ticker,
|
||||||
|
open_positions_for_symbol=2,
|
||||||
|
forecast=forecast,
|
||||||
|
account={"equity": 100.0, "symbol": "BTCUSDT", "symbol_exposure_usdt": 20.0},
|
||||||
|
)
|
||||||
|
|
||||||
|
first_sizing = first.diagnostics["position_sizing"]
|
||||||
|
second_sizing = second.diagnostics["position_sizing"]
|
||||||
|
assert first.action == "BUY"
|
||||||
|
assert first_sizing["method"] == "torch_forecast_fractional_kelly"
|
||||||
|
assert first_sizing["kelly_target_notional_usdt"] > settings.max_position_usdt
|
||||||
|
assert first.diagnostics["position_notional_usdt"] == settings.max_position_usdt
|
||||||
|
assert second.action == "BUY"
|
||||||
|
assert 1 <= second.diagnostics["position_notional_usdt"] < settings.max_position_usdt
|
||||||
|
assert second_sizing["kelly_open_symbol_exposure_usdt"] == 8.0
|
||||||
|
assert filled.action == "HOLD"
|
||||||
|
assert filled.diagnostics["checks"]["risk_size_ok"] is False
|
||||||
|
|
||||||
|
|
||||||
def test_torch_forecast_blocks_failed_quality_gate(make_settings, tmp_path) -> None:
|
def test_torch_forecast_blocks_failed_quality_gate(make_settings, tmp_path) -> None:
|
||||||
settings = make_settings(
|
settings = make_settings(
|
||||||
tmp_path,
|
tmp_path,
|
||||||
@@ -362,7 +428,7 @@ def test_torch_forecast_blocks_failed_quality_gate(make_settings, tmp_path) -> N
|
|||||||
assert signal.diagnostics["checks"]["quality_gate_ok"] is False
|
assert signal.diagnostics["checks"]["quality_gate_ok"] is False
|
||||||
|
|
||||||
|
|
||||||
def test_torch_forecast_probe_buys_on_positive_high_probability(make_settings, tmp_path) -> None:
|
def test_torch_forecast_probe_blocks_when_kelly_size_is_too_small(make_settings, tmp_path) -> None:
|
||||||
settings = make_settings(
|
settings = make_settings(
|
||||||
tmp_path,
|
tmp_path,
|
||||||
strategy_mode="torch_forecast",
|
strategy_mode="torch_forecast",
|
||||||
@@ -395,11 +461,11 @@ def test_torch_forecast_probe_buys_on_positive_high_probability(make_settings, t
|
|||||||
account={"equity": 100.0},
|
account={"equity": 100.0},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert signal.action == "BUY"
|
assert signal.action == "HOLD"
|
||||||
assert signal.diagnostics["edge_mode"] == "probe"
|
assert signal.diagnostics["edge_mode"] == "probe"
|
||||||
assert signal.diagnostics["checks"]["expected_edge_ok"] is True
|
assert signal.diagnostics["checks"]["expected_edge_ok"] is True
|
||||||
assert signal.diagnostics["position_sizing"]["edge_mode"] == "probe"
|
assert signal.diagnostics["checks"]["risk_size_ok"] is False
|
||||||
assert settings.min_position_usdt <= signal.diagnostics["position_notional_usdt"] < settings.max_position_usdt
|
assert signal.diagnostics["position_notional_usdt"] == 0.0
|
||||||
|
|
||||||
|
|
||||||
def test_torch_forecast_probe_blocks_negative_expected_return(make_settings, tmp_path) -> None:
|
def test_torch_forecast_probe_blocks_negative_expected_return(make_settings, tmp_path) -> None:
|
||||||
@@ -436,7 +502,7 @@ def test_torch_forecast_probe_blocks_negative_expected_return(make_settings, tmp
|
|||||||
assert signal.diagnostics["checks"]["expected_edge_ok"] is False
|
assert signal.diagnostics["checks"]["expected_edge_ok"] is False
|
||||||
|
|
||||||
|
|
||||||
def test_torch_forecast_rebound_overlay_buys_stabilized_drop(make_settings, tmp_path) -> None:
|
def test_torch_forecast_rebound_overlay_blocks_when_kelly_size_is_too_small(make_settings, tmp_path) -> None:
|
||||||
settings = make_settings(
|
settings = make_settings(
|
||||||
tmp_path,
|
tmp_path,
|
||||||
strategy_mode="torch_forecast",
|
strategy_mode="torch_forecast",
|
||||||
@@ -479,12 +545,12 @@ def test_torch_forecast_rebound_overlay_buys_stabilized_drop(make_settings, tmp_
|
|||||||
account={"equity": 100.0},
|
account={"equity": 100.0},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert signal.action == "BUY"
|
assert signal.action == "HOLD"
|
||||||
assert signal.diagnostics["entry_path"] == "rebound"
|
|
||||||
assert signal.diagnostics["rebound"]["active"] is True
|
assert signal.diagnostics["rebound"]["active"] is True
|
||||||
assert signal.diagnostics["edge_mode"] == "rebound"
|
assert signal.diagnostics["model_rebound_entry_ok"] is True
|
||||||
|
assert signal.diagnostics["rebound_entry_sized_ok"] is False
|
||||||
assert signal.diagnostics["checks"]["expected_edge_ok"] is False
|
assert signal.diagnostics["checks"]["expected_edge_ok"] is False
|
||||||
assert signal.diagnostics["position_notional_usdt"] <= settings.rebound_max_position_usdt
|
assert signal.diagnostics["checks"]["risk_size_ok"] is False
|
||||||
|
|
||||||
|
|
||||||
def test_torch_forecast_rebound_overlay_does_not_buy_negative_forecast(make_settings, tmp_path) -> None:
|
def test_torch_forecast_rebound_overlay_does_not_buy_negative_forecast(make_settings, tmp_path) -> None:
|
||||||
@@ -532,7 +598,7 @@ def test_torch_forecast_rebound_overlay_does_not_buy_negative_forecast(make_sett
|
|||||||
assert signal.diagnostics["edge_mode"] == "blocked"
|
assert signal.diagnostics["edge_mode"] == "blocked"
|
||||||
|
|
||||||
|
|
||||||
def test_torch_forecast_rebound_fallback_buys_when_symbol_has_no_model(make_settings, tmp_path) -> None:
|
def test_torch_forecast_rebound_fallback_blocks_when_kelly_size_is_too_small(make_settings, tmp_path) -> None:
|
||||||
settings = make_settings(
|
settings = make_settings(
|
||||||
tmp_path,
|
tmp_path,
|
||||||
strategy_mode="torch_forecast",
|
strategy_mode="torch_forecast",
|
||||||
@@ -571,12 +637,11 @@ def test_torch_forecast_rebound_fallback_buys_when_symbol_has_no_model(make_sett
|
|||||||
account={"equity": 100.0},
|
account={"equity": 100.0},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert signal.action == "BUY"
|
assert signal.action == "HOLD"
|
||||||
assert signal.diagnostics["entry_path"] == "rebound_fallback"
|
|
||||||
assert signal.diagnostics["fallback_rebound_entry_ok"] is True
|
assert signal.diagnostics["fallback_rebound_entry_ok"] is True
|
||||||
|
assert signal.diagnostics["rebound_entry_sized_ok"] is False
|
||||||
assert signal.diagnostics["missing_torch_model"] is True
|
assert signal.diagnostics["missing_torch_model"] is True
|
||||||
assert signal.diagnostics["edge_mode"] == "rebound_fallback"
|
assert signal.diagnostics["checks"]["risk_size_ok"] is False
|
||||||
assert signal.diagnostics["position_notional_usdt"] <= settings.rebound_max_position_usdt
|
|
||||||
|
|
||||||
|
|
||||||
def test_torch_forecast_rebound_fallback_can_be_disabled(make_settings, tmp_path) -> None:
|
def test_torch_forecast_rebound_fallback_can_be_disabled(make_settings, tmp_path) -> None:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
param(
|
param(
|
||||||
[string]$TaskName = "TradeBot PyTorch Forecaster Retrainer",
|
[string]$TaskName = "TradeBot PyTorch Forecaster Retrainer",
|
||||||
[int]$EveryHours = 6,
|
[int]$EveryHours = 6,
|
||||||
[string]$Symbols = "BTCUSDT,ETHUSDT,SOLUSDT,LTCUSDT",
|
[string]$Symbols = "",
|
||||||
[int]$Limit = 3000,
|
[int]$Limit = 3000,
|
||||||
[int]$Horizon = 0,
|
[int]$Horizon = 0,
|
||||||
[string]$Horizons = "",
|
[string]$Horizons = "",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
param(
|
param(
|
||||||
[int]$MinReplayTrades = 8,
|
[int]$MinReplayTrades = 8,
|
||||||
[int]$MaxAttempts = 0,
|
[int]$MaxAttempts = 0,
|
||||||
[string]$Symbols = "BTCUSDT,ETHUSDT,SOLUSDT,LTCUSDT",
|
[string]$Symbols = "",
|
||||||
[int]$Limit = 3000,
|
[int]$Limit = 3000,
|
||||||
[switch]$DeployToPi,
|
[switch]$DeployToPi,
|
||||||
[string]$PiHost = "192.168.0.185",
|
[string]$PiHost = "192.168.0.185",
|
||||||
@@ -119,10 +119,12 @@ while ($true) {
|
|||||||
"-NoProfile",
|
"-NoProfile",
|
||||||
"-ExecutionPolicy", "Bypass",
|
"-ExecutionPolicy", "Bypass",
|
||||||
"-File", $Runner,
|
"-File", $Runner,
|
||||||
"-Symbols", $Symbols,
|
|
||||||
"-Limit", $Limit.ToString(),
|
"-Limit", $Limit.ToString(),
|
||||||
"-Seed", $attemptSeed.ToString()
|
"-Seed", $attemptSeed.ToString()
|
||||||
)
|
)
|
||||||
|
if ($Symbols) {
|
||||||
|
$runnerArgs += @("-Symbols", $Symbols)
|
||||||
|
}
|
||||||
if ($DeployToPi) {
|
if ($DeployToPi) {
|
||||||
$runnerArgs += "-DeployToPi"
|
$runnerArgs += "-DeployToPi"
|
||||||
if ($PiHost) { $runnerArgs += @("-PiHost", $PiHost) }
|
if ($PiHost) { $runnerArgs += @("-PiHost", $PiHost) }
|
||||||
|
|||||||
Reference in New Issue
Block a user