feat: train forecasts on trade outcomes
This commit is contained in:
@@ -28,6 +28,7 @@ from crypto_spot_bot.indicators import add_indicators
|
||||
from crypto_spot_bot.models import Candle
|
||||
from crypto_spot_bot.time_series import (
|
||||
DEFAULT_TORCH_FEATURES,
|
||||
_barrier_outcome,
|
||||
_current_volatility_scale,
|
||||
_entry_horizon,
|
||||
_entry_output_layout,
|
||||
@@ -64,6 +65,7 @@ class ForecastRecord:
|
||||
future_net_percent: float
|
||||
benchmark_entry: bool
|
||||
benchmark_exit: bool
|
||||
take_profit_first: bool | None = None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -385,7 +387,22 @@ def _forecast_records(
|
||||
next_open = float(candles[index + 1].open)
|
||||
if next_open <= 0:
|
||||
continue
|
||||
future_log_return = math.log(closes[index + decision_horizon] / next_open) - round_trip_cost
|
||||
take_profit_first: bool | None = None
|
||||
if str(entry.get("target_transform", "")) == "barrier_net_return":
|
||||
outcome = _barrier_outcome(
|
||||
candles,
|
||||
end_index=index,
|
||||
horizon=decision_horizon,
|
||||
stop_loss_percent=_float_entry(entry, "target_stop_loss_percent", 0.04),
|
||||
take_profit_percent=_float_entry(entry, "target_take_profit_percent", 0.035),
|
||||
round_trip_cost=round_trip_cost,
|
||||
)
|
||||
if outcome is None:
|
||||
continue
|
||||
future_log_return, event = outcome
|
||||
take_profit_first = event >= 0.5
|
||||
else:
|
||||
future_log_return = math.log(closes[index + decision_horizon] / next_open) - round_trip_cost
|
||||
future_net_percent = (math.exp(future_log_return) - 1.0) * 100.0
|
||||
records.append(
|
||||
ForecastRecord(
|
||||
@@ -407,6 +424,7 @@ def _forecast_records(
|
||||
future_net_percent=future_net_percent,
|
||||
benchmark_entry=_benchmark_entry_signal(candles, trend_candles, index),
|
||||
benchmark_exit=_benchmark_exit_signal(candles, index),
|
||||
take_profit_first=take_profit_first,
|
||||
)
|
||||
)
|
||||
return records
|
||||
@@ -497,7 +515,22 @@ def _batch_forecast_records(
|
||||
next_open = float(candles[index + 1].open)
|
||||
if next_open <= 0:
|
||||
continue
|
||||
future_log_return = math.log(closes[index + decision_horizon] / next_open) - round_trip_cost
|
||||
take_profit_first: bool | None = None
|
||||
if str(entry.get("target_transform", "")) == "barrier_net_return":
|
||||
outcome = _barrier_outcome(
|
||||
candles,
|
||||
end_index=index,
|
||||
horizon=decision_horizon,
|
||||
stop_loss_percent=_float_entry(entry, "target_stop_loss_percent", 0.04),
|
||||
take_profit_percent=_float_entry(entry, "target_take_profit_percent", 0.035),
|
||||
round_trip_cost=round_trip_cost,
|
||||
)
|
||||
if outcome is None:
|
||||
continue
|
||||
future_log_return, event = outcome
|
||||
take_profit_first = event >= 0.5
|
||||
else:
|
||||
future_log_return = math.log(closes[index + decision_horizon] / next_open) - round_trip_cost
|
||||
future_net_percent = (math.exp(future_log_return) - 1.0) * 100.0
|
||||
records.append(
|
||||
ForecastRecord(
|
||||
@@ -519,6 +552,7 @@ def _batch_forecast_records(
|
||||
future_net_percent=future_net_percent,
|
||||
benchmark_entry=_benchmark_entry_signal(candles, trend_candles, index),
|
||||
benchmark_exit=_benchmark_exit_signal(candles, index),
|
||||
take_profit_first=take_profit_first,
|
||||
)
|
||||
)
|
||||
return records
|
||||
@@ -557,6 +591,10 @@ def _build_torch_model(entry: dict[str, Any], model_name: str) -> Any | None:
|
||||
output_size=output_size,
|
||||
attention_pooling=bool(entry.get("attention_pooling")),
|
||||
context_norm=bool(entry.get("context_norm")),
|
||||
multitask_head=bool(entry.get("multitask_head")),
|
||||
head_hidden_size=int(
|
||||
_clamp(_float_entry(entry, "head_hidden_size", float(hidden_size)), 8.0, 1024.0)
|
||||
),
|
||||
)
|
||||
raw_state = entry.get("state_dict")
|
||||
if not isinstance(raw_state, dict):
|
||||
@@ -566,12 +604,26 @@ def _build_torch_model(entry: dict[str, Any], model_name: str) -> Any | None:
|
||||
for key, value in raw_state.items()
|
||||
if isinstance(value, list)
|
||||
}
|
||||
head_weight = entry.get("head_weight")
|
||||
head_bias = entry.get("head_bias")
|
||||
if not isinstance(head_weight, list) or not isinstance(head_bias, list):
|
||||
return None
|
||||
state["head.weight"] = torch.tensor(head_weight, dtype=torch.float32)
|
||||
state["head.bias"] = torch.tensor(head_bias, dtype=torch.float32)
|
||||
if bool(entry.get("multitask_head")):
|
||||
for artifact_name, state_name in (
|
||||
("head_hidden_weight", "head_hidden.weight"),
|
||||
("head_hidden_bias", "head_hidden.bias"),
|
||||
("return_head_weight", "return_head.weight"),
|
||||
("return_head_bias", "return_head.bias"),
|
||||
("event_head_weight", "event_head.weight"),
|
||||
("event_head_bias", "event_head.bias"),
|
||||
):
|
||||
value = entry.get(artifact_name)
|
||||
if not isinstance(value, list):
|
||||
return None
|
||||
state[state_name] = torch.tensor(value, dtype=torch.float32)
|
||||
else:
|
||||
head_weight = entry.get("head_weight")
|
||||
head_bias = entry.get("head_bias")
|
||||
if not isinstance(head_weight, list) or not isinstance(head_bias, list):
|
||||
return None
|
||||
state["head.weight"] = torch.tensor(head_weight, dtype=torch.float32)
|
||||
state["head.bias"] = torch.tensor(head_bias, dtype=torch.float32)
|
||||
if bool(entry.get("attention_pooling")):
|
||||
attention_weight = entry.get("attention_weight")
|
||||
if not isinstance(attention_weight, list):
|
||||
@@ -639,10 +691,20 @@ def _decode_selected_output(
|
||||
expected = decode("mean")
|
||||
q_values = sorted([decode("q10", expected), decode("q50", expected), decode("q90", expected)])
|
||||
cap = _prediction_cap(history_closes, selected_horizon, round_trip_cost)
|
||||
if str(entry.get("target_transform", "")) == "barrier_net_return":
|
||||
stop_percent = _clamp(_float_entry(entry, "target_stop_loss_percent", 0.04), 0.003, 0.08)
|
||||
take_percent = _clamp(_float_entry(entry, "target_take_profit_percent", 0.035), 0.003, 0.20)
|
||||
cap = max(
|
||||
cap,
|
||||
abs(math.log(1.0 - stop_percent) - round_trip_cost),
|
||||
abs(math.log(1.0 + take_percent) - round_trip_cost),
|
||||
)
|
||||
return {
|
||||
"expected_return": _clamp(expected, -cap, cap),
|
||||
"q50": _clamp(q_values[1], -cap, cap),
|
||||
"probability_up": _sigmoid(float(values.get("logit_up", 0.0))),
|
||||
"probability_up": _sigmoid(
|
||||
float(values.get("logit_tp_first", values.get("logit_up", 0.0)))
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -1422,7 +1484,7 @@ def _fit_platt_calibration(records: list[ForecastRecord]) -> dict[str, float]:
|
||||
samples = [
|
||||
(
|
||||
math.log(_clamp(record.probability_up, 1e-5, 1.0 - 1e-5) / (1.0 - _clamp(record.probability_up, 1e-5, 1.0 - 1e-5))),
|
||||
1.0 if record.future_net_percent > 0 else 0.0,
|
||||
_record_event_target(record),
|
||||
)
|
||||
for record in records
|
||||
]
|
||||
@@ -1446,6 +1508,12 @@ def _fit_platt_calibration(records: list[ForecastRecord]) -> dict[str, float]:
|
||||
return {"slope": round(slope, 8), "intercept": round(intercept, 8), "samples": float(len(samples))}
|
||||
|
||||
|
||||
def _record_event_target(record: ForecastRecord) -> float:
|
||||
if record.take_profit_first is not None:
|
||||
return 1.0 if record.take_profit_first else 0.0
|
||||
return 1.0 if record.future_net_percent > 0 else 0.0
|
||||
|
||||
|
||||
def _apply_platt_calibration(
|
||||
records: list[ForecastRecord], calibration: dict[str, float]
|
||||
) -> list[ForecastRecord]:
|
||||
@@ -1544,6 +1612,9 @@ def _artifact_summary(artifact: dict[str, Any]) -> dict[str, Any]:
|
||||
"target_horizon": artifact.get("target_horizon"),
|
||||
"target_horizons": artifact.get("target_horizons"),
|
||||
"target_transform": artifact.get("target_transform"),
|
||||
"event_target": artifact.get("event_target"),
|
||||
"target_stop_loss_percent": artifact.get("target_stop_loss_percent"),
|
||||
"target_take_profit_percent": artifact.get("target_take_profit_percent"),
|
||||
"symbols": {
|
||||
symbol: {
|
||||
"model": row.get("model"),
|
||||
|
||||
Reference in New Issue
Block a user