Harden trading, training, and monitoring

This commit is contained in:
Codex
2026-07-10 15:51:53 +03:00
parent 6fb79ee2a9
commit 069d75d2f2
55 changed files with 2658 additions and 2332049 deletions
+68 -17
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import argparse
import hashlib
import json
import math
import sys
@@ -49,6 +50,10 @@ class ForecastRecord:
index: int
timestamp: int
close: float
high: float
low: float
next_open: float
next_timestamp: int
atr: float
expected_percent: float
probability_up: float
@@ -85,7 +90,9 @@ def main() -> None:
symbols = _symbols(args.symbols, settings.symbols)
context_symbols = sorted(set(symbols + _symbols(args.context_symbols, ())))
artifact_path = Path(args.artifact or settings.time_series_lstm_model_path)
artifact = json.loads(artifact_path.read_text(encoding="utf-8"))
artifact_bytes = artifact_path.read_bytes()
artifact_sha256 = hashlib.sha256(artifact_bytes).hexdigest()
artifact = json.loads(artifact_bytes.decode("utf-8"))
horizon = args.horizon if args.horizon > 0 else settings.time_series_forecast_horizon
round_trip_cost = _artifact_round_trip_cost(artifact, settings)
@@ -196,6 +203,7 @@ def main() -> None:
if args.output:
payload = {
"artifact_sha256": artifact_sha256,
"artifact": _artifact_summary(artifact),
"records_by_symbol": per_symbol_counts,
"recommended": _result_dict(recommended),
@@ -273,6 +281,11 @@ def _forecast_records(
decision_horizon = _entry_horizon(entry, horizon)
start = max(min_candles, int(float(entry.get("lookback", 64))))
end = len(candles) - decision_horizon - 1
holdout_start_timestamp = int(float(entry.get("holdout_start_timestamp", 0) or 0))
if holdout_start_timestamp <= 0:
return []
while start < end and candles[start].timestamp < holdout_start_timestamp:
start += 1
if calibration_window > 0:
start = max(start, end - calibration_window)
batched_records = _batch_forecast_records(
@@ -313,7 +326,10 @@ def _forecast_records(
q50 = float(selected.get("q50", expected_return))
expected_percent = (math.exp(expected_return) - 1.0) * 100.0
q50_percent = (math.exp(q50) - 1.0) * 100.0
future_log_return = math.log(closes[index + decision_horizon] / closes[index]) - round_trip_cost
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
future_net_percent = (math.exp(future_log_return) - 1.0) * 100.0
records.append(
ForecastRecord(
@@ -321,6 +337,10 @@ def _forecast_records(
index=index,
timestamp=candles[index].timestamp,
close=closes[index],
high=float(candles[index].high),
low=float(candles[index].low),
next_open=next_open,
next_timestamp=candles[index + 1].timestamp,
atr=float(candles[index].atr_14 or 0.0),
expected_percent=expected_percent,
probability_up=probability_up,
@@ -409,7 +429,10 @@ def _batch_forecast_records(
q50 = float(selected["q50"])
expected_percent = (math.exp(expected_return) - 1.0) * 100.0
q50_percent = (math.exp(q50) - 1.0) * 100.0
future_log_return = math.log(closes[index + decision_horizon] / closes[index]) - round_trip_cost
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
future_net_percent = (math.exp(future_log_return) - 1.0) * 100.0
records.append(
ForecastRecord(
@@ -417,6 +440,10 @@ def _batch_forecast_records(
index=index,
timestamp=candles[index].timestamp,
close=closes[index],
high=float(candles[index].high),
low=float(candles[index].low),
next_open=next_open,
next_timestamp=candles[index + 1].timestamp,
atr=float(candles[index].atr_14 or 0.0),
expected_percent=expected_percent,
probability_up=probability_up,
@@ -569,12 +596,13 @@ def _full_backtest(
rows: list[dict[str, Any]] = []
max_hold = max(12, horizon * 8)
stop_loss_percent = max(0.003, min(0.08, float(settings.stop_loss_percent))) * 100.0
take_profit_percent = max(0.003, min(0.20, float(settings.take_profit_percent))) * 100.0
stop_loss_exit_enabled = bool(getattr(settings, "stop_loss_exit_enabled", True))
atr_multiplier = max(0.5, min(10.0, float(settings.atr_trailing_multiplier)))
for record in sorted(records, key=lambda item: (item.timestamp, item.symbol)):
position = positions.get(record.symbol)
if position is not None:
position["highest"] = max(position["highest"], record.close)
position["highest"] = max(position["highest"], record.high)
net_percent = _net_percent(position["entry_price"], record.close, round_trip_cost)
held = record.index - int(position["entry_index"])
atr_stop_level = (
@@ -584,7 +612,7 @@ def _full_backtest(
)
atr_stop = bool(
atr_stop_level is not None
and record.close <= atr_stop_level
and record.low <= atr_stop_level
and (stop_loss_exit_enabled or atr_stop_level > position["entry_price"])
)
weak_forecast = (
@@ -593,10 +621,18 @@ def _full_backtest(
or record.skill <= 0.0
)
exit_reason = ""
if stop_loss_exit_enabled and net_percent <= -stop_loss_percent:
exit_price = record.close
stop_level = position["entry_price"] * (1.0 - stop_loss_percent / 100.0)
take_level = position["entry_price"] * (1.0 + take_profit_percent / 100.0)
if stop_loss_exit_enabled and record.low <= stop_level:
exit_reason = "stop_loss"
exit_price = stop_level
elif record.high >= take_level:
exit_reason = "take_profit"
exit_price = take_level
elif atr_stop:
exit_reason = "atr_trailing_stop"
exit_price = float(atr_stop_level)
elif (record.expected_percent <= 0.0 or record.probability_up <= 0.50 or _candidate_blocks(record, thresholds.edge)):
exit_reason = "forecast_negative"
elif weak_forecast and net_percent >= 0:
@@ -604,6 +640,7 @@ def _full_backtest(
elif held >= max_hold:
exit_reason = "max_hold"
if exit_reason:
net_percent = _net_percent(position["entry_price"], exit_price, round_trip_cost)
trades.append(net_percent)
rows.append(
{
@@ -624,10 +661,10 @@ def _full_backtest(
continue
if _candidate_allows(record, thresholds.edge, thresholds.probability, thresholds.confidence):
positions[record.symbol] = {
"entry_price": record.close,
"entry_index": record.index,
"timestamp": record.timestamp,
"highest": record.close,
"entry_price": record.next_open,
"entry_index": record.index + 1,
"timestamp": record.next_timestamp,
"highest": record.next_open,
"probability_up": record.probability_up,
"expected_percent": record.expected_percent,
}
@@ -669,12 +706,13 @@ def _benchmark_backtest(
rows: list[dict[str, Any]] = []
max_hold = max(12, horizon * 8)
stop_loss_percent = max(0.003, min(0.08, float(settings.stop_loss_percent))) * 100.0
take_profit_percent = max(0.003, min(0.20, float(settings.take_profit_percent))) * 100.0
stop_loss_exit_enabled = bool(getattr(settings, "stop_loss_exit_enabled", True))
atr_multiplier = max(0.5, min(10.0, float(settings.atr_trailing_multiplier)))
for record in sorted(records, key=lambda item: (item.timestamp, item.symbol)):
position = positions.get(record.symbol)
if position is not None:
position["highest"] = max(position["highest"], record.close)
position["highest"] = max(position["highest"], record.high)
net_percent = _net_percent(position["entry_price"], record.close, round_trip_cost)
held = record.index - int(position["entry_index"])
atr_stop_level = (
@@ -684,19 +722,28 @@ def _benchmark_backtest(
)
atr_stop = bool(
atr_stop_level is not None
and record.close <= atr_stop_level
and record.low <= atr_stop_level
and (stop_loss_exit_enabled or atr_stop_level > position["entry_price"])
)
exit_reason = ""
if stop_loss_exit_enabled and net_percent <= -stop_loss_percent:
exit_price = record.close
stop_level = position["entry_price"] * (1.0 - stop_loss_percent / 100.0)
take_level = position["entry_price"] * (1.0 + take_profit_percent / 100.0)
if stop_loss_exit_enabled and record.low <= stop_level:
exit_reason = "stop_loss"
exit_price = stop_level
elif record.high >= take_level:
exit_reason = "take_profit"
exit_price = take_level
elif atr_stop:
exit_reason = "atr_trailing_stop"
exit_price = float(atr_stop_level)
elif record.benchmark_exit:
exit_reason = "benchmark_exit"
elif held >= max_hold:
exit_reason = "max_hold"
if exit_reason:
net_percent = _net_percent(position["entry_price"], exit_price, round_trip_cost)
trades.append(net_percent)
rows.append(
{
@@ -715,10 +762,10 @@ def _benchmark_backtest(
continue
if record.benchmark_entry:
positions[record.symbol] = {
"entry_price": record.close,
"entry_index": record.index,
"timestamp": record.timestamp,
"highest": record.close,
"entry_price": record.next_open,
"entry_index": record.index + 1,
"timestamp": record.next_timestamp,
"highest": record.next_open,
}
for symbol, position in list(positions.items()):
tail = next((record for record in reversed(records) if record.symbol == symbol), None)
@@ -888,6 +935,7 @@ def _quality_gate(
return {
"status": "pass" if passed else "fail",
"passed": passed,
"protocol": "untouched_model_holdout_with_threshold_walk_forward",
"checks": checks,
"oos_summary": summary,
"benchmark_summary": benchmark_summary,
@@ -1216,6 +1264,9 @@ def _artifact_summary(artifact: dict[str, Any]) -> dict[str, Any]:
"lookback": row.get("lookback"),
"hidden_size": row.get("hidden_size"),
"skill": row.get("skill"),
"validation_skill": row.get("validation_skill"),
"holdout_skill": row.get("holdout_skill"),
"holdout_start_timestamp": row.get("holdout_start_timestamp"),
"directional_accuracy": row.get("directional_accuracy"),
}
for symbol, row in (artifact.get("symbols") or {}).items()