Add PyTorch recurrent forecaster
This commit is contained in:
@@ -171,6 +171,9 @@ def _validate_candidates(
|
||||
lstm_artifact: dict[str, Any] | None = None,
|
||||
) -> list[dict[str, float | str]]:
|
||||
models = ["naive", "drift", "ewma", "ar1", "ar3"]
|
||||
torch_model = _torch_recurrent_model_name(symbol, lstm_artifact or {})
|
||||
if torch_model and _can_use_torch_recurrent(returns, symbol, lstm_artifact or {}):
|
||||
models.append(torch_model)
|
||||
if _can_use_lstm(returns, settings, symbol, lstm_artifact or {}):
|
||||
models.append("lstm")
|
||||
rows: list[dict[str, float | str]] = []
|
||||
@@ -206,6 +209,8 @@ def _predict_next_return(
|
||||
return _ar_predict(returns, 1)
|
||||
if model == "ar3":
|
||||
return _ar_predict(returns, 3)
|
||||
if model in {"torch_lstm", "torch_gru"}:
|
||||
return _torch_recurrent_predict(returns, symbol, lstm_artifact or {})
|
||||
if model == "lstm" and settings is not None:
|
||||
return _lstm_predict(returns, settings, symbol, lstm_artifact or {})
|
||||
return 0.0
|
||||
@@ -285,6 +290,235 @@ def _clean_lstm_params(data: dict[str, Any]) -> dict[str, float | int]:
|
||||
return clean
|
||||
|
||||
|
||||
def _torch_recurrent_model_name(symbol: str | None, lstm_artifact: dict[str, Any]) -> str | None:
|
||||
entry = _torch_recurrent_entry(symbol, lstm_artifact)
|
||||
if not entry:
|
||||
return None
|
||||
architecture = str(entry.get("architecture", "")).strip().lower()
|
||||
if architecture in {"lstm", "gru"}:
|
||||
return f"torch_{architecture}"
|
||||
model = str(entry.get("model", "")).strip().lower()
|
||||
return model if model in {"torch_lstm", "torch_gru"} else None
|
||||
|
||||
|
||||
def _torch_recurrent_entry(symbol: str | None, lstm_artifact: dict[str, Any]) -> dict[str, Any] | None:
|
||||
symbols = lstm_artifact.get("symbols")
|
||||
entry = symbols.get(symbol.upper()) if symbol and isinstance(symbols, dict) else None
|
||||
if not isinstance(entry, dict):
|
||||
default = lstm_artifact.get("default")
|
||||
entry = default if isinstance(default, dict) else None
|
||||
if not isinstance(entry, dict):
|
||||
return None
|
||||
if not isinstance(entry.get("state_dict"), dict):
|
||||
return None
|
||||
return entry
|
||||
|
||||
|
||||
def _can_use_torch_recurrent(returns: list[float], symbol: str | None, lstm_artifact: dict[str, Any]) -> bool:
|
||||
entry = _torch_recurrent_entry(symbol, lstm_artifact)
|
||||
if not entry:
|
||||
return False
|
||||
lookback = int(_clamp(_float_entry(entry, "lookback", 0.0), 4.0, 512.0))
|
||||
hidden_size = int(_clamp(_float_entry(entry, "hidden_size", 0.0), 1.0, 512.0))
|
||||
num_layers = int(_clamp(_float_entry(entry, "num_layers", 1.0), 1.0, 8.0))
|
||||
return len(returns) >= lookback + 1 and hidden_size > 0 and num_layers > 0
|
||||
|
||||
|
||||
def _torch_recurrent_predict(
|
||||
returns: list[float],
|
||||
symbol: str | None,
|
||||
lstm_artifact: dict[str, Any],
|
||||
) -> float:
|
||||
entry = _torch_recurrent_entry(symbol, lstm_artifact)
|
||||
model_name = _torch_recurrent_model_name(symbol, lstm_artifact)
|
||||
if not entry or not model_name:
|
||||
return _predict_next_return("drift", returns)
|
||||
lookback = int(_clamp(_float_entry(entry, "lookback", 0.0), 4.0, 512.0))
|
||||
hidden_size = int(_clamp(_float_entry(entry, "hidden_size", 0.0), 1.0, 512.0))
|
||||
num_layers = int(_clamp(_float_entry(entry, "num_layers", 1.0), 1.0, 8.0))
|
||||
mean = _float_entry(entry, "mean", 0.0)
|
||||
scale = max(_float_entry(entry, "scale", _return_scale(returns)), 1e-8)
|
||||
clip = _clamp(_float_entry(entry, "clip", 8.0), 1.0, 50.0)
|
||||
if len(returns) < lookback:
|
||||
return _predict_next_return("drift", returns)
|
||||
|
||||
normalized = [_clamp((value - mean) / scale, -clip, clip) for value in returns[-lookback:]]
|
||||
try:
|
||||
hidden = _torch_recurrent_hidden(
|
||||
normalized,
|
||||
entry=entry,
|
||||
model_name=model_name,
|
||||
hidden_size=hidden_size,
|
||||
num_layers=num_layers,
|
||||
)
|
||||
if hidden is None:
|
||||
return _predict_next_return("drift", returns)
|
||||
head_weight = _float_vector(entry.get("head_weight"))
|
||||
head_bias = _float_entry(entry, "head_bias", 0.0)
|
||||
if len(head_weight) != hidden_size:
|
||||
return _predict_next_return("drift", returns)
|
||||
normalized_prediction = sum(weight * value for weight, value in zip(head_weight, hidden)) + head_bias
|
||||
if not math.isfinite(normalized_prediction):
|
||||
return _predict_next_return("drift", returns)
|
||||
prediction = _clamp(normalized_prediction, -clip, clip) * scale + mean
|
||||
except (IndexError, KeyError, TypeError, ValueError, OverflowError):
|
||||
return _predict_next_return("drift", returns)
|
||||
|
||||
recent_abs = sorted(abs(value) for value in returns[-48:]) if len(returns) >= 8 else [0.01]
|
||||
cap = max(recent_abs[int(len(recent_abs) * 0.9)], 0.0002)
|
||||
return _clamp(prediction, -cap, cap)
|
||||
|
||||
|
||||
def _torch_recurrent_hidden(
|
||||
normalized: list[float],
|
||||
*,
|
||||
entry: dict[str, Any],
|
||||
model_name: str,
|
||||
hidden_size: int,
|
||||
num_layers: int,
|
||||
) -> list[float] | None:
|
||||
state = entry.get("state_dict")
|
||||
if not isinstance(state, dict):
|
||||
return None
|
||||
h_layers = [[0.0 for _ in range(hidden_size)] for _ in range(num_layers)]
|
||||
c_layers = [[0.0 for _ in range(hidden_size)] for _ in range(num_layers)]
|
||||
for value in normalized:
|
||||
layer_input = [value]
|
||||
for layer in range(num_layers):
|
||||
if model_name == "torch_lstm":
|
||||
next_hidden, next_cell = _torch_lstm_step(layer_input, h_layers[layer], c_layers[layer], state, layer)
|
||||
h_layers[layer] = next_hidden
|
||||
c_layers[layer] = next_cell
|
||||
elif model_name == "torch_gru":
|
||||
h_layers[layer] = _torch_gru_step(layer_input, h_layers[layer], state, layer)
|
||||
else:
|
||||
return None
|
||||
layer_input = h_layers[layer]
|
||||
return h_layers[-1]
|
||||
|
||||
|
||||
def _torch_lstm_step(
|
||||
inputs: list[float],
|
||||
hidden: list[float],
|
||||
cell: list[float],
|
||||
state: dict[str, Any],
|
||||
layer: int,
|
||||
) -> tuple[list[float], list[float]]:
|
||||
hidden_size = len(hidden)
|
||||
gates = _torch_gate_values(inputs, hidden, state, layer, gate_count=4)
|
||||
input_gate = [_sigmoid(value) for value in gates[0]]
|
||||
forget_gate = [_sigmoid(value) for value in gates[1]]
|
||||
cell_gate = [math.tanh(value) for value in gates[2]]
|
||||
output_gate = [_sigmoid(value) for value in gates[3]]
|
||||
next_cell = [
|
||||
forget_gate[index] * cell[index] + input_gate[index] * cell_gate[index]
|
||||
for index in range(hidden_size)
|
||||
]
|
||||
next_hidden = [
|
||||
output_gate[index] * math.tanh(next_cell[index])
|
||||
for index in range(hidden_size)
|
||||
]
|
||||
return next_hidden, next_cell
|
||||
|
||||
|
||||
def _torch_gru_step(
|
||||
inputs: list[float],
|
||||
hidden: list[float],
|
||||
state: dict[str, Any],
|
||||
layer: int,
|
||||
) -> list[float]:
|
||||
hidden_size = len(hidden)
|
||||
weight_ih = _float_matrix(state[f"weight_ih_l{layer}"])
|
||||
weight_hh = _float_matrix(state[f"weight_hh_l{layer}"])
|
||||
bias_ih = _float_vector(state[f"bias_ih_l{layer}"])
|
||||
bias_hh = _float_vector(state[f"bias_hh_l{layer}"])
|
||||
|
||||
def gate_input(gate: int) -> list[float]:
|
||||
start = gate * hidden_size
|
||||
output = []
|
||||
for index in range(hidden_size):
|
||||
row = start + index
|
||||
output.append(_dot(weight_ih[row], inputs) + bias_ih[row])
|
||||
return output
|
||||
|
||||
def gate_hidden(gate: int) -> list[float]:
|
||||
start = gate * hidden_size
|
||||
output = []
|
||||
for index in range(hidden_size):
|
||||
row = start + index
|
||||
output.append(_dot(weight_hh[row], hidden) + bias_hh[row])
|
||||
return output
|
||||
|
||||
reset_input = gate_input(0)
|
||||
update_input = gate_input(1)
|
||||
new_input = gate_input(2)
|
||||
reset_hidden = gate_hidden(0)
|
||||
update_hidden = gate_hidden(1)
|
||||
new_hidden = gate_hidden(2)
|
||||
|
||||
reset_gate = [_sigmoid(reset_input[index] + reset_hidden[index]) for index in range(hidden_size)]
|
||||
update_gate = [_sigmoid(update_input[index] + update_hidden[index]) for index in range(hidden_size)]
|
||||
candidate = [
|
||||
math.tanh(new_input[index] + reset_gate[index] * new_hidden[index])
|
||||
for index in range(hidden_size)
|
||||
]
|
||||
return [
|
||||
(1 - update_gate[index]) * candidate[index] + update_gate[index] * hidden[index]
|
||||
for index in range(hidden_size)
|
||||
]
|
||||
|
||||
|
||||
def _torch_gate_values(
|
||||
inputs: list[float],
|
||||
hidden: list[float],
|
||||
state: dict[str, Any],
|
||||
layer: int,
|
||||
gate_count: int,
|
||||
) -> list[list[float]]:
|
||||
hidden_size = len(hidden)
|
||||
weight_ih = _float_matrix(state[f"weight_ih_l{layer}"])
|
||||
weight_hh = _float_matrix(state[f"weight_hh_l{layer}"])
|
||||
bias_ih = _float_vector(state[f"bias_ih_l{layer}"])
|
||||
bias_hh = _float_vector(state[f"bias_hh_l{layer}"])
|
||||
gates: list[list[float]] = []
|
||||
for gate in range(gate_count):
|
||||
values = []
|
||||
start = gate * hidden_size
|
||||
for index in range(hidden_size):
|
||||
row = start + index
|
||||
values.append(_dot(weight_ih[row], inputs) + _dot(weight_hh[row], hidden) + bias_ih[row] + bias_hh[row])
|
||||
gates.append(values)
|
||||
return gates
|
||||
|
||||
|
||||
def _float_entry(data: dict[str, Any], key: str, default: float) -> float:
|
||||
value = data.get(key)
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value)
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return default
|
||||
return default
|
||||
|
||||
|
||||
def _float_vector(data: Any) -> list[float]:
|
||||
if not isinstance(data, list):
|
||||
return []
|
||||
return [float(value) for value in data]
|
||||
|
||||
|
||||
def _float_matrix(data: Any) -> list[list[float]]:
|
||||
if not isinstance(data, list):
|
||||
return []
|
||||
return [_float_vector(row) for row in data]
|
||||
|
||||
|
||||
def _dot(left: list[float], right: list[float]) -> float:
|
||||
return sum(left[index] * right[index] for index in range(min(len(left), len(right))))
|
||||
|
||||
|
||||
def _lstm_predict(
|
||||
returns: list[float],
|
||||
settings: Settings,
|
||||
|
||||
Reference in New Issue
Block a user