chore: scaffold uv project, config, personas, risk, model profiles
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DvdRZkyCy9NJpHYDdPqmS
This commit is contained in:
commit
52450c0835
12 files changed
+2696
No files matched your search
@@ -0,0 +1,137 @@
|
||||
"""Central configuration. Single source of truth for keys, budgets, timezones, paths.
|
||||
|
||||
Reads from the repo-root ``.env`` (see ``.env.example``). Env var names map to the
|
||||
snake_case fields below case-insensitively (pydantic-settings default).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
from .models import ModelProfile, resolve_profile
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=REPO_ROOT / ".env",
|
||||
env_file_encoding="utf-8",
|
||||
extra="ignore",
|
||||
case_sensitive=False,
|
||||
)
|
||||
|
||||
# --- Alpaca: one paper account per persona (single-account fields are a fallback) ---
|
||||
alpaca_paper: bool = True
|
||||
alpaca_api_key: str = ""
|
||||
alpaca_secret_key: str = ""
|
||||
alpaca_nova_key: str = ""
|
||||
alpaca_nova_secret: str = ""
|
||||
alpaca_warren_lite_key: str = ""
|
||||
alpaca_warren_lite_secret: str = ""
|
||||
alpaca_atlas_key: str = ""
|
||||
alpaca_atlas_secret: str = ""
|
||||
|
||||
# --- Finnhub ---
|
||||
finnhub_api_key: str = ""
|
||||
|
||||
# --- oMLX local server (Apple MLX; OpenAI-compatible; auto-starts on login) ---
|
||||
llm_host: str = "127.0.0.1"
|
||||
llm_port: int = 8090
|
||||
# oMLX enforces an API key. Blank → auto-read from ~/.omlx/settings.json (auth.api_key).
|
||||
omlx_api_key: str = ""
|
||||
# Model toggle (presets in config/models.py): gpt_oss | qwen35_27b
|
||||
model_profile: str = "gpt_oss"
|
||||
# Optional explicit served model id; blank = use the profile + resolve against /v1/models.
|
||||
llm_model: str = ""
|
||||
llm_max_tokens: int = 2048
|
||||
|
||||
# --- Budgets ---
|
||||
per_agent_wallclock_min: int = 130
|
||||
per_agent_step_budget: int = 60
|
||||
per_agent_min_steps: int = 25 # keep researching (nudge) until at least this many steps
|
||||
hard_stop: str = "07:00" # local wall-clock HH:MM; nightly run must finish by this
|
||||
|
||||
# --- Timezones ---
|
||||
market_tz: str = "America/New_York" # authoritative market clock
|
||||
local_tz: str = "America/Chicago" # this Mac
|
||||
|
||||
# --- Derived paths ---
|
||||
@property
|
||||
def state_dir(self) -> Path:
|
||||
return REPO_ROOT / "state"
|
||||
|
||||
@property
|
||||
def output_dir(self) -> Path:
|
||||
return REPO_ROOT / "state" / "output"
|
||||
|
||||
@property
|
||||
def log_dir(self) -> Path:
|
||||
return REPO_ROOT / "state" / "logs"
|
||||
|
||||
@property
|
||||
def db_path(self) -> Path:
|
||||
return REPO_ROOT / "state" / "trading.db"
|
||||
|
||||
@property
|
||||
def llm_base_url(self) -> str:
|
||||
return f"http://{self.llm_host}:{self.llm_port}/v1"
|
||||
|
||||
@property
|
||||
def llm_api_key(self) -> str:
|
||||
"""oMLX API key: explicit env value, else auto-discovered from oMLX's settings.json."""
|
||||
if self.omlx_api_key:
|
||||
return self.omlx_api_key
|
||||
try:
|
||||
import json
|
||||
|
||||
data = json.loads((Path.home() / ".omlx" / "settings.json").read_text())
|
||||
return data["auth"]["api_key"]
|
||||
except Exception:
|
||||
return "not-needed"
|
||||
|
||||
@property
|
||||
def active_profile(self) -> ModelProfile:
|
||||
return resolve_profile(self.model_profile)
|
||||
|
||||
@property
|
||||
def effective_model_id(self) -> str:
|
||||
"""Explicit override if set, else the active profile's served id.
|
||||
The client further resolves this against oMLX's /v1/models."""
|
||||
return self.llm_model or self.active_profile.served_model_id
|
||||
|
||||
@property
|
||||
def llm_context_length(self) -> int:
|
||||
return self.active_profile.context_length
|
||||
|
||||
@property
|
||||
def llm_temperature(self) -> float:
|
||||
return self.active_profile.temperature
|
||||
|
||||
def alpaca_creds(self, persona_id: str) -> tuple[str, str]:
|
||||
"""(key, secret) for a persona's paper account, falling back to the single-account keys."""
|
||||
key = getattr(self, f"alpaca_{persona_id}_key", "") or ""
|
||||
secret = getattr(self, f"alpaca_{persona_id}_secret", "") or ""
|
||||
if key and secret:
|
||||
return key, secret
|
||||
return self.alpaca_api_key, self.alpaca_secret_key
|
||||
|
||||
def alpaca_data_creds(self) -> tuple[str, str]:
|
||||
"""Any valid creds — Alpaca market data is account-agnostic."""
|
||||
for pid in ("nova", "warren_lite", "atlas"):
|
||||
key, secret = self.alpaca_creds(pid)
|
||||
if key and secret:
|
||||
return key, secret
|
||||
return self.alpaca_api_key, self.alpaca_secret_key
|
||||
|
||||
def ensure_dirs(self) -> None:
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
Reference in new issue
Block a user