-
Notifications
You must be signed in to change notification settings - Fork 0
Add initial BrainAlpha CLI scaffolding #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Gucc111
wants to merge
1
commit into
exp/try_codex
Choose a base branch
from
codex/create-python-cli-for-worldquant-brain
base: exp/try_codex
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -205,3 +205,6 @@ cython_debug/ | |
| marimo/_static/ | ||
| marimo/_lsp/ | ||
| __marimo__/ | ||
|
|
||
| # BrainAlpha local data | ||
| .brainalpha/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| # BrainAlpha | ||
|
|
||
| A Python CLI toolbox for automating WorldQuant Brain alpha research workflows. The project aims to turn repetitive | ||
| web actions—creating alphas, running backtests, and submitting qualified strategies—into a scriptable pipeline that can | ||
| also collaborate with LLMs. | ||
|
|
||
| ## Vision & Scope | ||
| - Manage credentials and API access for WorldQuant Brain. | ||
| - Provide CLI commands for alpha CRUD, backtests, submissions, and data schema discovery. | ||
| - Integrate LLMs to propose or refine alpha expressions based on field metadata and prior performance. | ||
| - Enable end-to-end pipelines: generate → validate → backtest → qualify → submit. | ||
| - Keep the architecture cleanly layered (infra, services, CLI) so future Web/App clients can reuse the core services. | ||
|
|
||
| ## Project Milestones | ||
| - **M1:** Project skeleton, configuration system, logging, and CLI scaffolding. | ||
| - **M2:** WorldQuant Brain API client with alpha CRUD and backtests. | ||
| - **M3:** LLM-driven alpha generation with validation. | ||
| - **M4:** Automated qualify-and-submit pipeline driven by config files. | ||
| - **M5:** Documentation and stable service interfaces for downstream clients. | ||
|
|
||
| ## Getting Started | ||
|
|
||
| ### Installation (editable) | ||
| ```bash | ||
| pip install --editable . | ||
| ``` | ||
|
|
||
| ### Commands | ||
| - `brainalpha version` — show installed version. | ||
| - `brainalpha config init` — write credentials and defaults to `~/.brainalpha/config.yml`. | ||
| - `brainalpha config show` — view saved configuration (secrets are redacted). | ||
|
|
||
| ## Configuration | ||
| Configuration is stored at `~/.brainalpha/config.yml` by default. The file captures: | ||
|
|
||
| - `api_key`: WorldQuant Brain API key (kept private; file permissions set to `600`). | ||
| - `base_url`: API base URL (defaults to production endpoint). | ||
| - `storage_path`: Local directory for caching artifacts and backtests. | ||
|
|
||
| You can override the config path by instantiating `ConfigManager` with a custom path. | ||
|
|
||
| ## Development Notes | ||
| - Packaging uses [`hatchling`](https://hatch.pypa.io/latest/). | ||
| - CLI built on [`typer`](https://typer.tiangolo.com/) and [`rich`](https://rich.readthedocs.io/). | ||
| - HTTP interactions use [`httpx`](https://www.python-httpx.org/) with a lightweight retry helper. | ||
|
|
||
| ## Roadmap | ||
| See `configs/sample.yml` for an example of future pipeline configuration. Upcoming work will flesh out API services, | ||
| backtest orchestration, qualification rules, and LLM prompt templates. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # Example pipeline configuration for BrainAlpha | ||
| alpha: | ||
| name: sample_alpha | ||
| expression: "rank(ts_mean(close, 10) - ts_mean(close, 60))" | ||
| tags: | ||
| - demo | ||
| workflow: | ||
| generate_with_llm: false | ||
| backtest: | ||
| start_date: 2020-01-01 | ||
| end_date: 2024-01-01 | ||
| universe: top_3000 | ||
| qualification: | ||
| sharpe_min: 1.0 | ||
| turnover_max: 0.6 | ||
| submit_if_qualified: false |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| [build-system] | ||
| requires = ["hatchling"] | ||
| build-backend = "hatchling.build" | ||
|
|
||
| [project] | ||
| name = "brainalpha" | ||
| version = "0.1.0" | ||
| description = "CLI tooling for automating WorldQuant Brain alpha workflows" | ||
| authors = [{ name = "BrainAlphaLab", email = "devnull@example.com" }] | ||
| readme = "README.md" | ||
| requires-python = ">=3.10" | ||
| dependencies = [ | ||
| "typer[all]>=0.12", | ||
| "rich>=13.7", | ||
| "httpx>=0.26", | ||
| "pyyaml>=6.0", | ||
| ] | ||
|
|
||
| [project.scripts] | ||
| brainalpha = "brainalpha.cli:run" | ||
|
|
||
| [tool.hatch.build.targets.wheel] | ||
| packages = ["src/brainalpha"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| """BrainAlpha package initializer.""" | ||
|
|
||
| __app_name__ = "brainalpha" | ||
| __version__ = "0.1.0" | ||
|
|
||
| __all__ = ["__app_name__", "__version__"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| """Command-line interface for BrainAlpha.""" | ||
| from __future__ import annotations | ||
|
|
||
| import typer | ||
| from rich.console import Console | ||
|
|
||
| from brainalpha import __app_name__, __version__ | ||
| from brainalpha.cli.config import config_app | ||
| from brainalpha.logging import configure_logging | ||
|
|
||
| console = Console() | ||
| app = typer.Typer(help="CLI toolkit for WorldQuant Brain alpha workflows") | ||
| app.add_typer(config_app, name="config") | ||
|
|
||
|
|
||
| @app.callback() | ||
| def main(ctx: typer.Context) -> None: | ||
| """Configure global options and logging.""" | ||
| configure_logging() | ||
| ctx.obj = {} | ||
|
|
||
|
|
||
| @app.command() | ||
| def version() -> None: | ||
| """Show the installed version of brainalpha.""" | ||
| console.print(f"{__app_name__} v{__version__}") | ||
|
|
||
|
|
||
| def run() -> None: | ||
| """Entry point for console_scripts.""" | ||
| app() | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| run() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| """Configuration management commands.""" | ||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
| from typing import Optional | ||
|
|
||
| import typer | ||
| from rich.console import Console | ||
| from rich.table import Table | ||
|
|
||
| from brainalpha.config.manager import BrainAlphaConfig, ConfigManager | ||
|
|
||
| console = Console() | ||
| config_app = typer.Typer(help="Manage authentication and runtime configuration") | ||
|
|
||
|
|
||
| @config_app.command("init") | ||
| def init_config( | ||
| api_key: str = typer.Option( | ||
| "", prompt="WorldQuant Brain API key", hide_input=True, confirmation_prompt=True | ||
| ), | ||
| base_url: str = typer.Option( | ||
| "https://www.worldquantbrain.com/api", prompt="API base URL" | ||
| ), | ||
| storage_path: Optional[Path] = typer.Option( | ||
| None, | ||
| help="Optional override for local data storage path.", | ||
| ), | ||
| ) -> None: | ||
| """Initialize the local configuration file with credentials and defaults.""" | ||
| manager = ConfigManager() | ||
| config = BrainAlphaConfig(api_key=api_key, base_url=base_url, storage_path=storage_path) | ||
| manager.save(config) | ||
| console.print(f"Configuration written to {manager.config_path}") | ||
|
|
||
|
|
||
| @config_app.command("show") | ||
| def show_config() -> None: | ||
| """Display the current configuration (hiding secrets).""" | ||
| manager = ConfigManager() | ||
| config = manager.load() | ||
| table = Table(title="BrainAlpha Configuration") | ||
| table.add_column("Field") | ||
| table.add_column("Value") | ||
| table.add_row("base_url", config.base_url) | ||
| table.add_row("api_key", "***" if config.api_key else "(not set)") | ||
| table.add_row("storage_path", str(config.storage_path)) | ||
| console.print(table) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| """Configuration package for BrainAlpha.""" | ||
|
|
||
| from .manager import BrainAlphaConfig, ConfigManager | ||
|
|
||
| __all__ = ["BrainAlphaConfig", "ConfigManager"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| """Configuration data classes and persistence utilities.""" | ||
| from __future__ import annotations | ||
|
|
||
| import os | ||
| from dataclasses import dataclass, field | ||
| from pathlib import Path | ||
| from typing import Optional | ||
|
|
||
| import yaml | ||
|
|
||
| DEFAULT_CONFIG_DIR = Path.home() / ".brainalpha" | ||
| DEFAULT_CONFIG_PATH = DEFAULT_CONFIG_DIR / "config.yml" | ||
| DEFAULT_STORAGE_PATH = DEFAULT_CONFIG_DIR / "storage" | ||
|
|
||
|
|
||
| @dataclass | ||
| class BrainAlphaConfig: | ||
| """User configuration for accessing WorldQuant Brain APIs.""" | ||
|
|
||
| api_key: str = "" | ||
| base_url: str = "https://www.worldquantbrain.com/api" | ||
| storage_path: Path = field(default_factory=lambda: DEFAULT_STORAGE_PATH) | ||
|
|
||
| def to_dict(self) -> dict: | ||
| data = { | ||
| "api_key": self.api_key, | ||
| "base_url": self.base_url, | ||
| "storage_path": str(self.storage_path), | ||
| } | ||
| return data | ||
|
|
||
| @classmethod | ||
| def from_dict(cls, raw: dict) -> "BrainAlphaConfig": | ||
| return cls( | ||
| api_key=raw.get("api_key", ""), | ||
| base_url=raw.get("base_url", "https://www.worldquantbrain.com/api"), | ||
| storage_path=Path(raw.get("storage_path", DEFAULT_STORAGE_PATH)), | ||
| ) | ||
|
|
||
|
|
||
| class ConfigManager: | ||
| """Load and persist configuration to disk.""" | ||
|
|
||
| def __init__(self, config_path: Optional[Path] = None) -> None: | ||
| self.config_path = config_path or DEFAULT_CONFIG_PATH | ||
| self.config_dir = self.config_path.parent | ||
|
|
||
| def ensure_config_dir(self) -> None: | ||
| self.config_dir.mkdir(parents=True, exist_ok=True) | ||
|
|
||
| def load(self) -> BrainAlphaConfig: | ||
| if not self.config_path.exists(): | ||
| return BrainAlphaConfig() | ||
| with self.config_path.open("r", encoding="utf-8") as f: | ||
| data = yaml.safe_load(f) or {} | ||
| return BrainAlphaConfig.from_dict(data) | ||
|
|
||
| def save(self, config: BrainAlphaConfig) -> None: | ||
| self.ensure_config_dir() | ||
| with self.config_path.open("w", encoding="utf-8") as f: | ||
| yaml.safe_dump(config.to_dict(), f, default_flow_style=False) | ||
| if "posix" in os.name: | ||
| os.chmod(self.config_path, 0o600) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| """Infrastructure utilities for external integrations.""" | ||
|
|
||
| from .http import BrainHttpClient | ||
|
|
||
| __all__ = ["BrainHttpClient"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| """HTTP client abstraction for WorldQuant Brain API access.""" | ||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| import time | ||
| from typing import Any, Dict, Optional | ||
|
|
||
| import httpx | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| DEFAULT_TIMEOUT = 15.0 | ||
| DEFAULT_RETRIES = 3 | ||
| RETRY_BACKOFF = 2.0 | ||
|
|
||
|
|
||
| class BrainHttpClient: | ||
| """Simple HTTP client with retry and base URL handling.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| base_url: str, | ||
| api_key: str = "", | ||
| timeout: float = DEFAULT_TIMEOUT, | ||
| retries: int = DEFAULT_RETRIES, | ||
| ) -> None: | ||
| self.base_url = base_url.rstrip("/") | ||
| self.api_key = api_key | ||
| self.timeout = timeout | ||
| self.retries = retries | ||
| self._client = httpx.Client(timeout=timeout) | ||
|
|
||
| def _headers(self) -> Dict[str, str]: | ||
| headers: Dict[str, str] = { | ||
| "User-Agent": "brainalpha-cli/0.1", | ||
| } | ||
| if self.api_key: | ||
| headers["Authorization"] = f"Bearer {self.api_key}" | ||
| return headers | ||
|
|
||
| def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response: | ||
| url = f"{self.base_url}/{path.lstrip('/')}" | ||
| kwargs.setdefault("headers", {}).update(self._headers()) | ||
|
|
||
| attempt = 0 | ||
| while True: | ||
| attempt += 1 | ||
| try: | ||
| response = self._client.request(method, url, **kwargs) | ||
| if response.status_code in (429, 500, 502, 503, 504) and attempt <= self.retries: | ||
| delay = RETRY_BACKOFF ** (attempt - 1) | ||
| logger.warning( | ||
| "Received %s for %s %s, retrying in %.1fs (attempt %s/%s)", | ||
| response.status_code, | ||
| method, | ||
| url, | ||
| delay, | ||
| attempt, | ||
| self.retries, | ||
| ) | ||
| time.sleep(delay) | ||
| continue | ||
| response.raise_for_status() | ||
| return response | ||
| except httpx.HTTPStatusError: | ||
| raise | ||
| except httpx.HTTPError as exc: # network level | ||
| if attempt > self.retries: | ||
| raise | ||
| delay = RETRY_BACKOFF ** (attempt - 1) | ||
| logger.warning( | ||
| "HTTP error for %s %s: %s. Retrying in %.1fs (attempt %s/%s)", | ||
| method, | ||
| url, | ||
| exc, | ||
| delay, | ||
| attempt, | ||
| self.retries, | ||
| ) | ||
| time.sleep(delay) | ||
|
|
||
| def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> httpx.Response: | ||
| return self._request("GET", path, params=params) | ||
|
|
||
| def post(self, path: str, json: Optional[Dict[str, Any]] = None) -> httpx.Response: | ||
| return self._request("POST", path, json=json) | ||
|
|
||
| def patch(self, path: str, json: Optional[Dict[str, Any]] = None) -> httpx.Response: | ||
| return self._request("PATCH", path, json=json) | ||
|
|
||
| def delete(self, path: str) -> httpx.Response: | ||
| return self._request("DELETE", path) | ||
|
|
||
| def close(self) -> None: | ||
| self._client.close() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| """Logging configuration utilities.""" | ||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| import sys | ||
|
|
||
| from rich.logging import RichHandler | ||
|
|
||
| LOG_LEVEL = logging.INFO | ||
|
|
||
|
|
||
| def configure_logging(level: int = LOG_LEVEL) -> None: | ||
| """Configure application-wide logging with a consistent format.""" | ||
| if logging.getLogger().handlers: | ||
| return | ||
|
|
||
| logging.basicConfig( | ||
| level=level, | ||
| format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s", | ||
| datefmt="%Y-%m-%d %H:%M:%S", | ||
| handlers=[RichHandler(rich_tracebacks=True, console=None, markup=True, stream=sys.stderr)], | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Service layer placeholder for domain operations.""" |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: None value bypasses default factory, corrupts storage path
When users run
brainalpha config initwithout specifying--storage-path, thestorage_pathparameter isNoneand gets passed explicitly toBrainAlphaConfig. The dataclass'sdefault_factoryonly triggers when the argument is omitted entirely, not whenNoneis passed. This causesto_dict()to serializestorage_pathas the string"None", and when reloaded viafrom_dict(), it createsPath("None")- a literal path named "None" instead of the intended default storage location.Additional Locations (1)
src/brainalpha/config/manager.py#L21-L22