chart/app/config.py
Chris Amow 5ccb2bfb52 Add Schwab settings, credential placeholders and an entitlement probe
config.py had no Schwab fields at all, so the keys listed in .env.example were
being silently dropped by extra="ignore". They exist now, blank, and nothing
reads them while live_source is yahoo.

The token path moves under data/, which is the Coolify persistent volume. Left
at the repository root it would vanish on every rebuild, and re-authenticating
is an interactive browser flow, not something a deploy can do for itself.

scripts/check_schwab.py answers empirically what the app is entitled to rather
than inferring it from documentation: whether the credentials authenticate,
whether /ES quotes return (futures market data is a separate entitlement from
equities), and whether the streamer bootstrap responds.

That last one is the decision. StreamClient.login() reads
/trader/v1/userPreference for its socket URL and credentials, and that path
belongs to the Accounts and Trading product — so an app registered for Market
Data Production alone cannot stream, and CHART_FUTURES is unreachable until the
app adds it. The script reports which of the two paths is open instead of
leaving it to be discovered halfway through an implementation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 04:48:29 -05:00

71 lines
2.7 KiB
Python

from pathlib import Path
from pydantic_settings import BaseSettings, SettingsConfigDict
from app.bars.models import Timeframe
TIMEFRAME_WEIGHT = {
Timeframe.M1: 1,
Timeframe.M2: 1,
Timeframe.M5: 1,
Timeframe.M15: 2,
Timeframe.M30: 3,
Timeframe.H1: 4,
Timeframe.D1: 16,
}
MA_WEIGHT_FACTOR = 0.75
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
live_source: str = "yahoo"
seed_source: str = "yahoo"
yahoo_symbol: str = "ES=F"
yahoo_poll_seconds: float = 20
seed_1h_range: str = "730d"
seed_1m_range: str = "8d"
timeframes: str = "1m,5m,15m,30m,1h,1d"
base_timeframes: str = "1m,30m,1d"
max_bars_per_tf: int = 5000
ma_sets__1d: str = "sma10,sma20,sma50,sma100,sma200"
daily_anchor_et: str = "18:00"
manual_lines_path: Path = Path("./data/manual_lines.json")
# Schwab. Empty until the app's keys are issued; nothing reads them while
# live_source is yahoo. The token lives under data/ so it lands on the
# Coolify persistent volume — a rebuild would otherwise log you out, and
# re-authenticating is an interactive browser flow.
schwab_api_key: str = ""
schwab_app_secret: str = ""
schwab_callback_url: str = "https://chart.amow.com/api/qt"
schwab_token_path: Path = Path("./data/.schwab_token.json")
schwab_symbol: str = "/ES"
confluence_min_score: float = 28
# Four hours, chosen from the sweep in scripts/calibrate_alerts.py. Suppression is
# per price zone, so an unrelated zone still alerts immediately; this only
# governs how often the *same* area repeats itself.
alert_cooldown_seconds: int = 14400
ntfy_topic: str = ""
ntfy_server: str = "https://ntfy.sh"
chart_auth_token: str = ""
replay_file: Path | None = None
@property
def enabled_timeframes(self) -> list[Timeframe]:
return [Timeframe(value.strip()) for value in self.timeframes.split(",") if value.strip()]
@property
def ma_sets(self) -> dict[Timeframe, list[tuple[str, int]]]:
configured = {Timeframe.D1: self.ma_sets__1d}
result: dict[Timeframe, list[tuple[str, int]]] = {}
for tf, value in configured.items():
definitions = []
for item in filter(None, (part.strip().lower() for part in value.split(","))):
kind = "sma" if item.startswith("sma") else "ema" if item.startswith("ema") else ""
if not kind or not item[len(kind) :].isdigit():
raise ValueError(f"Invalid MA definition: {item}")
definitions.append((kind, int(item[len(kind) :])))
result[tf] = definitions
return result