Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions PAPER_TRADING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Paper trading Kronos 4h

Estado: activo como servicios de usuario, sin claves de exchange y sin capacidad
para enviar órdenes reales.

## Regla congelada

- Mercado: BTCUSDT, velas cerradas de 4 horas de Binance.
- Cuatro tendencias long-only: EMA 20/100, 20/200, 50/100 y 50/200.
- Cada componente usa 12,5 %; exposición máxima total 50 %, sin apalancamiento.
- Una entrada sólo se permite cuando la desviación entre tres predicciones Kronos
no supera el percentil 90 de las últimas 1.080 observaciones.
- Cada componente sale con su cruce bajista.
- Se registra simultáneamente un control con las mismas tendencias y costes pero
sin filtro Kronos, para medir el aporte incremental real de la IA.
- Simulación incluye 0,10 % de comisión y 0,02 % de slippage por lado.
- Corte diario: 4 %. Parada definitiva del experimento: drawdown del 20 %.

## Consultar

```bash
cd /home/ocultwolf/newBot/kronos
kronos_env/bin/python Kronos/examples/paper_status.py
systemctl --user status kronos-api.service kronos-paper-trader.service
journalctl --user -u kronos-paper-trader.service -n 30 --no-pager
kronos_env/bin/python Kronos/examples/audit_paper_journal.py
```

Datos persistentes:

- `outputs/paper_kronos_trend_4h/state.json`
- `outputs/paper_kronos_trend_4h/journal.csv`
- `outputs/paper_kronos_trend_4h/audit.json`
- `outputs/kronos_research_4h_3y/paper_candidate_validation/report.json`

## Puerta antes de dinero real

No se considerará conexión a un exchange hasta cumplir simultáneamente:

1. mínimo 90 días de ejecución forward sin cambiar la regla;
2. al menos dos ciclos completos de entrada y salida;
3. rentabilidad neta positiva y drawdown no superior al 10 % en paper;
4. ninguna vela perdida o duplicada y servicios estables;
5. nueva revisión humana explícita de resultados, límites y tamaño inicial.

Incluso superando estos puntos no existe garantía de beneficio. El primer capital,
si se autoriza posteriormente, deberá ser pequeño y no necesario para gastos.
551 changes: 551 additions & 0 deletions data/XSHG_5min_600977.csv

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions deploy/kronos-api.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[Unit]
Description=Kronos local GPU inference API
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=/home/ocultwolf/newBot/kronos/Kronos
ExecStart=/usr/bin/env -u HSA_OVERRIDE_GFX_VERSION /home/ocultwolf/newBot/kronos/kronos_env/bin/python examples/api_agent.py
Restart=on-failure
RestartSec=10

[Install]
WantedBy=default.target
7 changes: 7 additions & 0 deletions deploy/kronos-paper-audit.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[Unit]
Description=Audit Kronos paper trading journal

[Service]
Type=oneshot
WorkingDirectory=/home/ocultwolf/newBot/kronos
ExecStart=/home/ocultwolf/newBot/kronos/kronos_env/bin/python Kronos/examples/audit_paper_journal.py
10 changes: 10 additions & 0 deletions deploy/kronos-paper-audit.timer
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Unit]
Description=Daily Kronos paper trading integrity audit

[Timer]
OnCalendar=daily
Persistent=true
Unit=kronos-paper-audit.service

[Install]
WantedBy=timers.target
14 changes: 14 additions & 0 deletions deploy/kronos-paper-trader.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[Unit]
Description=Kronos BTCUSDT 4h paper trader
After=network-online.target kronos-api.service
Wants=network-online.target kronos-api.service

[Service]
Type=simple
WorkingDirectory=/home/ocultwolf/newBot/kronos
ExecStart=/home/ocultwolf/newBot/kronos/kronos_env/bin/python Kronos/examples/paper_trade_kronos_trend_4h.py --loop --poll-seconds 300
Restart=always
RestartSec=30

[Install]
WantedBy=default.target
68 changes: 68 additions & 0 deletions examples/api_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Guardar como examples/api_agent.py
import sys
from pathlib import Path

from flask import Flask, request, jsonify
import numpy as np
import pandas as pd
import torch

# Permite ejecutar directamente: python examples/api_agent.py
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))

from model import Kronos, KronosTokenizer, KronosPredictor

app = Flask(__name__)

# Inicializar modelo de forma global
tokenizer = KronosTokenizer.from_pretrained("NeoQuasar/Kronos-Tokenizer-base")
model = Kronos.from_pretrained("NeoQuasar/Kronos-base")
tokenizer.eval()
model.eval()
predictor = KronosPredictor(model, tokenizer, max_context=512)

@app.route('/predict', methods=['POST'])
def get_prediction():
data = request.json
seed = data.get('seed')
if seed is not None:
seed = int(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
# Convertir el JSON entrante de OpenClaw a DataFrame de Pandas
df = pd.DataFrame(data['history'])
df['timestamps'] = pd.to_datetime(df['timestamps'])

# Extraer timestamps futuros (simulados o enviados por el bot)
pred_len = data.get('pred_len', 120)
# Generar marcas de tiempo futuras basadas en la frecuencia (ej. 5min)
freq = data.get('freq', '5min')
# En pandas, "m" significa fin de mes; Yahoo usa "m" para minutos.
if isinstance(freq, str) and freq.endswith('m') and freq[:-1].isdigit():
freq = f"{freq[:-1]}min"
y_timestamp = pd.Series(
pd.date_range(
start=df['timestamps'].iloc[-1],
periods=pred_len + 1,
freq=freq,
)[1:]
)

# Inferencia en GPU AMD (vía PyTorch ROCm)
pred_df = predictor.predict(
df=df[['open', 'high', 'low', 'close', 'volume', 'amount']],
x_timestamp=df['timestamps'],
y_timestamp=y_timestamp,
pred_len=pred_len
)

pred_df.index.name = 'timestamps'
return jsonify(pred_df.reset_index().to_dict(orient='records'))

if __name__ == '__main__':
# Sólo localhost: esta API no tiene autenticación y no debe exponerse a la red.
app.run(host='127.0.0.1', port=7080, threaded=False)
115 changes: 115 additions & 0 deletions examples/audit_paper_journal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""Audita integridad y puerta de capital del journal de paper trading."""

from __future__ import annotations

import json
from pathlib import Path

import numpy as np
import pandas as pd

ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "outputs" / "paper_kronos_trend_4h"
ONE_SIDE_COST = 0.0012


def main() -> int:
state = json.loads((OUT / "state.json").read_text(encoding="utf-8"))
journal = pd.read_csv(OUT / "journal.csv")
journal["bar_time"] = pd.to_datetime(journal["bar_time"], utc=True)
errors: list[str] = []
if not journal["bar_time"].is_monotonic_increasing:
errors.append("timestamps desordenados")
if not journal["bar_time"].is_unique:
errors.append("velas duplicadas")
gaps = journal["bar_time"].diff().dropna()
if len(gaps) and not (gaps == pd.Timedelta(hours=4)).all():
errors.append("faltan velas de 4h")
if not np.isfinite(journal[["close", "equity", "drawdown"]].to_numpy()).all():
errors.append("valores no finitos")
exposure_units = journal["new_exposure"] / 0.125
if not (((exposure_units - exposure_units.round()).abs() < 1e-9) & exposure_units.between(0, 4)).all():
errors.append("exposición fuera de los cuatro componentes")
if ((~journal["kronos_risk_ok"].astype(bool)) & (journal["new_exposure"] > journal["previous_exposure"] + 1e-12)).any():
errors.append("entrada realizada con filtro Kronos bloqueado")

initial = float(state["initial_equity"])
expected = initial
previous_close = None
for row in journal.itertuples(index=False):
if previous_close is not None:
expected *= 1 + float(row.previous_exposure) * (float(row.close) / previous_close - 1)
expected *= 1 - float(row.turnover) * ONE_SIDE_COST
if not np.isclose(expected, float(row.equity), rtol=1e-10, atol=1e-6):
errors.append(f"equity incoherente en {row.bar_time}")
break
previous_close = float(row.close)
if "control_equity" in journal:
expected_control = initial
previous_close = None
for row in journal.itertuples(index=False):
control_value = getattr(row, "control_equity", float("nan"))
if pd.isna(control_value):
previous_close = float(row.close)
continue
if previous_close is not None:
expected_control *= 1 + float(row.control_previous_exposure) * (float(row.close) / previous_close - 1)
expected_control *= 1 - float(row.control_turnover) * ONE_SIDE_COST
if not np.isclose(expected_control, float(control_value), rtol=1e-10, atol=1e-6):
errors.append(f"equity del control incoherente en {row.bar_time}")
break
previous_close = float(row.close)
if str(journal.iloc[-1]["bar_time"].isoformat()) != str(pd.Timestamp(state["last_processed_bar"]).isoformat()):
errors.append("state y journal no coinciden en la última vela")

starts = ((journal["previous_exposure"] == 0) & (journal["new_exposure"] > 0)).sum()
completed = ((journal["previous_exposure"] > 0) & (journal["new_exposure"] == 0)).sum()
elapsed_days = (journal.iloc[-1]["bar_time"] - journal.iloc[0]["bar_time"]).total_seconds() / 86400
current_return = float(journal.iloc[-1]["equity"] / initial - 1)
max_drawdown = float(journal["drawdown"].min())
control_available = "control_equity" in journal and journal["control_equity"].notna().any()
control_return = None
kronos_incremental = None
if control_available:
control_rows = journal[journal["control_equity"].notna()].copy()
control_start = float(control_rows.iloc[0]["control_equity"])
control_end = float(control_rows.iloc[-1]["control_equity"])
control_return = control_end / control_start - 1
candidate_same_start = float(control_rows.iloc[0]["equity"])
candidate_same_return = float(control_rows.iloc[-1]["equity"]) / candidate_same_start - 1
kronos_incremental = candidate_same_return - control_return
ready = bool(
not errors
and elapsed_days >= 90
and completed >= 2
and current_return > 0
and max_drawdown >= -0.10
and control_available
and kronos_incremental is not None
and kronos_incremental >= 0
and not bool(state["halted"])
)
report = {
"integrity_ok": not errors,
"errors": errors,
"bars": len(journal),
"elapsed_days": elapsed_days,
"entries_from_flat": int(starts),
"completed_cycles": int(completed),
"return_pct": current_return * 100,
"max_drawdown_pct": max_drawdown * 100,
"control_return_pct": None if control_return is None else control_return * 100,
"kronos_incremental_pct_points": None if kronos_incremental is None else kronos_incremental * 100,
"capital_review_ready": ready,
"remaining": {
"days_to_90": max(0.0, 90 - elapsed_days),
"cycles_to_2": max(0, 2 - int(completed)),
},
}
(OUT / "audit.json").write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
print(json.dumps(report, indent=2, ensure_ascii=False))
return 0 if not errors else 1


if __name__ == "__main__":
raise SystemExit(main())
Loading