Harden trading, training, and monitoring
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user