Skip to content
Merged
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
16 changes: 16 additions & 0 deletions OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,22 @@ python -m bot.profile_runner paper validate btc
python -m bot.profile_runner live validate btc
```

Validate actual paper broker and market-data access without placing an order:

```bash
python -m bot.profile_runner paper connectivity btc
```

EC2 deployment runs this connectivity check before installing cron. Invalid or expired credentials therefore fail deployment instead of producing a validation-only database that looks healthy.

The paper BTC cron installation creates three jobs by default:

- trading cycle every 5 minutes
- monitor report hourly at minute 17
- research replay daily at 00:42 ET

Override these with `CRON_SCHEDULE`, `MONITOR_CRON_SCHEDULE`, and `RESEARCH_CRON_SCHEDULE` when installing cron.

Current runtime defaults:

- entry stale-bar blocking is disabled unless `ENABLE_STALE_BAR_CHECK=1`
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,14 @@ python -m bot.profile_runner paper trade spy
python -m bot.profile_runner live trade spy
```

Before relying on a schedule, verify both the paper account and market-data feed (this never places an order):

```bash
python -m bot.profile_runner paper connectivity btc
```

The BTC paper profile is an exploration profile: it permits up to 8 entries per day with a one-bar cooldown and slightly looser ADX/volume gates, while reducing target notional to 25% and ATR risk sizing to 1%. Live BTC settings are unchanged. The EC2 paper schedule refreshes its monitor hourly and its historical research report daily.

## Run

```bash
Expand Down
10 changes: 9 additions & 1 deletion bot/profile_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@


def _usage() -> int:
print("Usage: python -m bot.profile_runner <paper|live|paper-btc|live-btc> <trade|monitor|research|optimize|validate> [spy|btc]")
print(
"Usage: python -m bot.profile_runner <paper|live|paper-btc|live-btc> "
"<trade|monitor|research|optimize|validate|connectivity> [spy|btc]"
)
return 2


Expand Down Expand Up @@ -48,6 +51,11 @@ def main(argv: list[str] | None = None) -> int:

return int(validate_main())

if action == "connectivity":
from bot.validate_connectivity import main as connectivity_main

return int(connectivity_main())

return _usage()


Expand Down
76 changes: 70 additions & 6 deletions bot/report_monitor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import json
import os
import sqlite3
from datetime import datetime, timedelta, timezone

Expand Down Expand Up @@ -191,12 +192,68 @@ def _latest_metric_lines(latest_run) -> list[str]:
return [f"- Strategy metrics: {'; '.join(parts)}"]


def _runtime_health(
runs: pd.DataFrame,
max_age_minutes: int = 15,
now: datetime | None = None,
) -> dict:
checked_at = pd.Timestamp(now or datetime.now(timezone.utc))
if checked_at.tzinfo is None:
checked_at = checked_at.tz_localize("UTC")
else:
checked_at = checked_at.tz_convert("UTC")

empty_status = "no_real_runs"
if runs.empty or "ts" not in runs.columns:
return {
"healthy": False,
"status": empty_status,
"latest_real_run_ts": None,
"age_minutes": None,
"max_age_minutes": max_age_minutes,
}

notes = runs.get("note", pd.Series("", index=runs.index)).fillna("").astype(str)
real_runs = runs[~notes.str.contains("runtime_validation_sample", regex=False)]
if real_runs.empty:
return {
"healthy": False,
"status": "validation_only",
"latest_real_run_ts": None,
"age_minutes": None,
"max_age_minutes": max_age_minutes,
}

latest_ts = pd.to_datetime(real_runs["ts"], utc=True, errors="coerce").dropna().max()
if pd.isna(latest_ts):
return {
"healthy": False,
"status": empty_status,
"latest_real_run_ts": None,
"age_minutes": None,
"max_age_minutes": max_age_minutes,
}

age_minutes = max(0.0, (checked_at - latest_ts).total_seconds() / 60.0)
healthy = age_minutes <= max_age_minutes
return {
"healthy": healthy,
"status": "healthy" if healthy else "stale",
"latest_real_run_ts": latest_ts,
"age_minutes": age_minutes,
"max_age_minutes": max_age_minutes,
}


def _near_miss_rows(runs: pd.DataFrame, limit: int = 10) -> list[dict]:
if runs.empty:
return []

near_misses: list[dict] = []
for row in runs.tail(500).itertuples():
note = str(getattr(row, "note", "") or "")
if "runtime_validation_sample" in note:
continue
signal = str(getattr(row, "signal", "") or "")
action = str(getattr(row, "desired_action", "") or "")
try:
Expand Down Expand Up @@ -290,6 +347,10 @@ def main() -> None:
closed = add_condition_buckets(closed)

latest_run = runs.iloc[-1] if not runs.empty else None
runtime_health = _runtime_health(
runs,
max_age_minutes=max(1, int(os.getenv("HEALTH_MAX_RUN_AGE_MINUTES", "15"))),
)
latest_events = events.tail(20) if not events.empty else pd.DataFrame()
pending_orders = orders[orders["processed_at"].isna()] if not orders.empty and "processed_at" in orders.columns else pd.DataFrame()
summary = closed_trade_summary(closed)
Expand Down Expand Up @@ -382,6 +443,12 @@ def main() -> None:
)
latest_run_lines.extend(_latest_metric_lines(latest_run))

health_lines = [
f"- Status: {'HEALTHY' if runtime_health['healthy'] else 'UNHEALTHY'} ({runtime_health['status']})",
f"- Latest real trading cycle: {_fmt_ts(runtime_health['latest_real_run_ts'])}",
f"- Cycle age: {_fmt_num(runtime_health['age_minutes'], 1)} minutes (limit {runtime_health['max_age_minutes']})",
]

profit_factor_text = "n/a" if summary["profit_factor"] is None else f"{summary['profit_factor']:.2f}"
lines = [
"# Monitor Report",
Expand All @@ -391,12 +458,8 @@ def main() -> None:
"## Latest Run",
]
lines.extend(latest_run_lines if latest_run_lines else ["- No runs recorded yet."])
lines.extend(
[
"",
"## Bot State",
]
)
lines.extend(["", "## Runtime Health", *health_lines])
lines.extend(["", "## Bot State"])
lines.extend(state_lines if state_lines else ["- No state row found yet."])
lines.extend(
[
Expand Down Expand Up @@ -475,6 +538,7 @@ def main() -> None:
"latest_run": None if latest_run is None else latest_run.to_dict(),
"state": [] if state.empty else state.to_dict(orient="records"),
"positions": [] if positions.empty else positions.to_dict(orient="records"),
"runtime_health": runtime_health,
"pending_orders": [] if pending_orders.empty else pending_orders.to_dict(orient="records"),
"rejection_counts": rejection_counts,
"near_misses": near_misses,
Expand Down
73 changes: 73 additions & 0 deletions bot/validate_connectivity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from __future__ import annotations

import os
import sys
from dataclasses import dataclass

from bot.broker_alpaca import get_recent_bars, make_clients


@dataclass(frozen=True)
class ConnectivityResult:
symbol: str
paper: bool
equity: float
bar_count: int
latest_bar_ts: str


def _env_flag(name: str, default: bool = False) -> bool:
raw = os.getenv(name)
if raw is None:
return default
return raw.strip().lower() in {"1", "true", "yes", "on"}


def validate_connectivity() -> ConnectivityResult:
"""Verify broker authentication and market-data access without placing an order."""
symbol = os.getenv("SYMBOL", "SPY").strip().upper()
timeframe_minutes = int(os.getenv("TIMEFRAME_MINUTES", "5"))
paper = _env_flag("ALPACA_PAPER", True)

trading, data = make_clients()
try:
account = trading.get_account()
equity = float(account.equity)
except Exception as exc:
raise RuntimeError(
f"Alpaca {'paper' if paper else 'live'} trading authentication failed for {symbol}. "
"Refresh the profile credentials before installing or trusting the schedule."
) from exc

bars = get_recent_bars(data, symbol, timeframe_minutes, limit=3)
if bars.empty:
raise RuntimeError(
f"Alpaca market-data connectivity failed for {symbol}; no recent bars were returned. "
"Check the credentials, symbol, and data entitlement."
)

return ConnectivityResult(
symbol=symbol,
paper=paper,
equity=equity,
bar_count=len(bars),
latest_bar_ts=str(bars.index[-1]),
)


def main() -> int:
try:
result = validate_connectivity()
except RuntimeError as exc:
print(f"connectivity failed: {exc}", file=sys.stderr)
return 1
mode = "paper" if result.paper else "live"
print(
f"connectivity ok: mode={mode} symbol={result.symbol} equity={result.equity:.2f} "
f"bars={result.bar_count} latest_bar={result.latest_bar_ts}"
)
return 0


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