alpha-visualizer 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alpha_visualizer/__init__.py +3 -0
- alpha_visualizer/app.py +73 -0
- alpha_visualizer/cli.py +66 -0
- alpha_visualizer/db.py +61 -0
- alpha_visualizer/forge_config.py +133 -0
- alpha_visualizer/routers/__init__.py +0 -0
- alpha_visualizer/routers/ideas.py +57 -0
- alpha_visualizer/routers/results.py +352 -0
- alpha_visualizer/routers/strategies.py +272 -0
- alpha_visualizer/routers/wfo.py +126 -0
- alpha_visualizer/static/assets/index-16K3BxVS.css +1 -0
- alpha_visualizer/static/assets/index-dB6krL5g.js +11 -0
- alpha_visualizer/static/assets/index-dB6krL5g.js.map +1 -0
- alpha_visualizer/static/assets/inter-tight-latin-400-normal-BLrFJfvD.woff +0 -0
- alpha_visualizer/static/assets/inter-tight-latin-400-normal-iW8qmuJY.woff2 +0 -0
- alpha_visualizer/static/assets/inter-tight-latin-500-normal-BFXNXuvF.woff2 +0 -0
- alpha_visualizer/static/assets/inter-tight-latin-500-normal-pobXraBK.woff +0 -0
- alpha_visualizer/static/assets/inter-tight-latin-600-normal-BgSTtRxb.woff2 +0 -0
- alpha_visualizer/static/assets/inter-tight-latin-600-normal-D7bG6gX1.woff +0 -0
- alpha_visualizer/static/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- alpha_visualizer/static/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- alpha_visualizer/static/assets/source-serif-4-latin-600-normal-DMD1h6_f.woff +0 -0
- alpha_visualizer/static/assets/source-serif-4-latin-600-normal-DouSKlru.woff2 +0 -0
- alpha_visualizer/static/favicon.svg +1 -0
- alpha_visualizer/static/icons.svg +24 -0
- alpha_visualizer/static/index.html +33 -0
- alpha_visualizer-0.1.0.dist-info/METADATA +66 -0
- alpha_visualizer-0.1.0.dist-info/RECORD +30 -0
- alpha_visualizer-0.1.0.dist-info/WHEEL +4 -0
- alpha_visualizer-0.1.0.dist-info/entry_points.txt +2 -0
alpha_visualizer/app.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""FastAPI アプリケーションファクトリ"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import pathlib
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
8
|
+
|
|
9
|
+
from alpha_visualizer.forge_config import ForgeConfig
|
|
10
|
+
from alpha_visualizer.routers import ideas as ideas_router
|
|
11
|
+
from alpha_visualizer.routers import results as results_router
|
|
12
|
+
from alpha_visualizer.routers import strategies as strategies_router
|
|
13
|
+
from alpha_visualizer.routers import wfo as wfo_router
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_app(
|
|
17
|
+
forge_dir: pathlib.Path | None = None,
|
|
18
|
+
*,
|
|
19
|
+
config: ForgeConfig | None = None,
|
|
20
|
+
) -> FastAPI:
|
|
21
|
+
"""FastAPI アプリを生成する。
|
|
22
|
+
|
|
23
|
+
引数は次のいずれかを満たす必要がある:
|
|
24
|
+
- ``config``: 解決済みの ``ForgeConfig`` を直接渡す(推奨)
|
|
25
|
+
- ``forge_dir``: ディレクトリパスを渡し、内部で ``ForgeConfig.from_forge_dir`` を呼ぶ
|
|
26
|
+
(後方互換)
|
|
27
|
+
|
|
28
|
+
両方渡された場合は ``config`` が優先される。
|
|
29
|
+
"""
|
|
30
|
+
if config is None:
|
|
31
|
+
if forge_dir is None:
|
|
32
|
+
raise ValueError("forge_dir または config のいずれかを指定してください")
|
|
33
|
+
config = ForgeConfig.from_forge_dir(pathlib.Path(forge_dir))
|
|
34
|
+
|
|
35
|
+
app = FastAPI(
|
|
36
|
+
title="alpha-visualizer",
|
|
37
|
+
description="AlphaForge バックテスト結果の Web 可視化ツール",
|
|
38
|
+
version="0.1.0",
|
|
39
|
+
)
|
|
40
|
+
app.state.forge_config = config
|
|
41
|
+
|
|
42
|
+
app.include_router(results_router.router, prefix="/api")
|
|
43
|
+
app.include_router(strategies_router.router, prefix="/api")
|
|
44
|
+
app.include_router(ideas_router.router, prefix="/api")
|
|
45
|
+
app.include_router(wfo_router.router, prefix="/api")
|
|
46
|
+
|
|
47
|
+
forge_dir_str = str(config.forge_dir)
|
|
48
|
+
|
|
49
|
+
@app.get("/health")
|
|
50
|
+
async def health() -> JSONResponse:
|
|
51
|
+
return JSONResponse({"status": "ok", "forge_dir": forge_dir_str})
|
|
52
|
+
|
|
53
|
+
static_dir = pathlib.Path(__file__).parent / "static"
|
|
54
|
+
if static_dir.exists():
|
|
55
|
+
index_html = static_dir / "index.html"
|
|
56
|
+
static_root = static_dir.resolve()
|
|
57
|
+
|
|
58
|
+
@app.get("/{full_path:path}")
|
|
59
|
+
async def spa_fallback(full_path: str) -> FileResponse:
|
|
60
|
+
"""SPA ルート対応: /api 配下以外で実ファイルがあればそれを返し、
|
|
61
|
+
無ければ index.html を返して history mode の直アクセス・リロードに対応する。
|
|
62
|
+
"""
|
|
63
|
+
requested = (static_dir / full_path).resolve()
|
|
64
|
+
# ディレクトリトラバーサル対策
|
|
65
|
+
try:
|
|
66
|
+
requested.relative_to(static_root)
|
|
67
|
+
except ValueError:
|
|
68
|
+
return FileResponse(index_html)
|
|
69
|
+
if requested.is_file():
|
|
70
|
+
return FileResponse(requested)
|
|
71
|
+
return FileResponse(index_html)
|
|
72
|
+
|
|
73
|
+
return app
|
alpha_visualizer/cli.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""vis CLI エントリーポイント"""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from alpha_visualizer import __version__
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.group()
|
|
9
|
+
@click.version_option(version=__version__, prog_name="alpha-visualizer")
|
|
10
|
+
def cli() -> None:
|
|
11
|
+
"""alpha-visualizer - AlphaForge バックテスト結果の Web 可視化ツール"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@cli.command("serve")
|
|
15
|
+
@click.option("--host", default="127.0.0.1", show_default=True, help="バインドするホスト名")
|
|
16
|
+
@click.option("--port", default=8000, show_default=True, help="ポート番号")
|
|
17
|
+
@click.option(
|
|
18
|
+
"--forge-dir",
|
|
19
|
+
default=".",
|
|
20
|
+
show_default=True,
|
|
21
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
22
|
+
help="forge が生成するデータディレクトリのパス",
|
|
23
|
+
)
|
|
24
|
+
@click.option(
|
|
25
|
+
"--forge-config",
|
|
26
|
+
"forge_config",
|
|
27
|
+
default=None,
|
|
28
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False),
|
|
29
|
+
help="forge.yaml のパス。未指定なら $FORGE_CONFIG → <forge-dir>/forge.yaml の順で探索",
|
|
30
|
+
)
|
|
31
|
+
@click.option("--no-open", "no_open", is_flag=True, default=False, help="ブラウザを自動で開かない")
|
|
32
|
+
def serve(
|
|
33
|
+
host: str,
|
|
34
|
+
port: int,
|
|
35
|
+
forge_dir: str,
|
|
36
|
+
forge_config: str | None,
|
|
37
|
+
no_open: bool,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Web ダッシュボードを起動する"""
|
|
40
|
+
import pathlib
|
|
41
|
+
|
|
42
|
+
import uvicorn
|
|
43
|
+
|
|
44
|
+
from alpha_visualizer.app import create_app
|
|
45
|
+
from alpha_visualizer.forge_config import ForgeConfig
|
|
46
|
+
|
|
47
|
+
forge_path = pathlib.Path(forge_dir).resolve()
|
|
48
|
+
config_path = pathlib.Path(forge_config).resolve() if forge_config else None
|
|
49
|
+
config = ForgeConfig.from_forge_dir(forge_path, config_path=config_path)
|
|
50
|
+
app = create_app(config=config)
|
|
51
|
+
|
|
52
|
+
url = f"http://{host}:{port}"
|
|
53
|
+
click.echo(f"vis serve: {url} (Ctrl+C で停止)")
|
|
54
|
+
click.echo(f"forge-dir: {forge_path}")
|
|
55
|
+
click.echo(f"forge-db: {config.forge_db}")
|
|
56
|
+
if config.strategies_db is not None:
|
|
57
|
+
click.echo(f"strategies-db: {config.strategies_db}")
|
|
58
|
+
else:
|
|
59
|
+
click.echo(f"strategies-dir: {config.strategies_dir} (JSON モード)")
|
|
60
|
+
|
|
61
|
+
if not no_open:
|
|
62
|
+
import webbrowser
|
|
63
|
+
webbrowser.open(url)
|
|
64
|
+
|
|
65
|
+
uvicorn.run(app, host=host, port=port)
|
|
66
|
+
click.echo("vis serve を停止しました。")
|
alpha_visualizer/db.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""forge.db / strategies.db の SQLAlchemy テーブル定義(読み取り専用)"""
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import REAL, Column, Integer, MetaData, Table, Text
|
|
4
|
+
|
|
5
|
+
metadata = MetaData()
|
|
6
|
+
|
|
7
|
+
strategies = Table(
|
|
8
|
+
"strategies",
|
|
9
|
+
metadata,
|
|
10
|
+
Column("id", Integer, primary_key=True),
|
|
11
|
+
Column("strategy_id", Text, nullable=False, unique=True),
|
|
12
|
+
Column("name", Text, nullable=False),
|
|
13
|
+
Column("version", Text),
|
|
14
|
+
Column("asset_type", Text),
|
|
15
|
+
Column("timeframe", Text),
|
|
16
|
+
Column("tags", Text),
|
|
17
|
+
Column("notes", Text),
|
|
18
|
+
Column("definition_json", Text, nullable=False),
|
|
19
|
+
Column("source_file", Text),
|
|
20
|
+
Column("created_at", Text),
|
|
21
|
+
Column("updated_at", Text),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
backtest_results = Table(
|
|
25
|
+
"backtest_results",
|
|
26
|
+
metadata,
|
|
27
|
+
Column("run_id", Text, primary_key=True),
|
|
28
|
+
Column("strategy_id", Text),
|
|
29
|
+
Column("symbol", Text),
|
|
30
|
+
Column("run_at", Text),
|
|
31
|
+
Column("total_return_pct", REAL),
|
|
32
|
+
Column("cagr_pct", REAL),
|
|
33
|
+
Column("sharpe_ratio", REAL),
|
|
34
|
+
Column("sortino_ratio", REAL),
|
|
35
|
+
Column("calmar_ratio", REAL),
|
|
36
|
+
Column("max_drawdown_pct", REAL),
|
|
37
|
+
Column("total_trades", Integer),
|
|
38
|
+
Column("win_rate_pct", REAL),
|
|
39
|
+
Column("profit_factor", REAL),
|
|
40
|
+
Column("avg_holding_days", REAL),
|
|
41
|
+
Column("metrics_json", Text),
|
|
42
|
+
Column("equity_curve_json", Text),
|
|
43
|
+
Column("buy_hold_curve_json", Text),
|
|
44
|
+
Column("trades_json", Text),
|
|
45
|
+
Column("oos_start", Text),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
optimization_runs = Table(
|
|
49
|
+
"optimization_runs",
|
|
50
|
+
metadata,
|
|
51
|
+
Column("run_id", Text, primary_key=True),
|
|
52
|
+
Column("strategy_id", Text),
|
|
53
|
+
Column("symbol", Text),
|
|
54
|
+
Column("run_at", Text),
|
|
55
|
+
Column("n_trials", Integer),
|
|
56
|
+
Column("best_metric_name", Text),
|
|
57
|
+
Column("best_metric_value", REAL),
|
|
58
|
+
Column("best_params_json", Text),
|
|
59
|
+
Column("duration_seconds", REAL),
|
|
60
|
+
Column("all_trials_json", Text),
|
|
61
|
+
)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""forge.yaml を読み取り、forge が生成するディレクトリ構造からパスを解決するデータクラス
|
|
2
|
+
|
|
3
|
+
forge.yaml の探索順序:
|
|
4
|
+
1. 引数 ``config_path`` が明示指定されていればそれ
|
|
5
|
+
2. 環境変数 ``FORGE_CONFIG`` が指すパス(存在する場合のみ)
|
|
6
|
+
3. ``<forge_dir>/forge.yaml``
|
|
7
|
+
4. 見つからなければ既存ハードコード値(後方互換)
|
|
8
|
+
|
|
9
|
+
forge.yaml 内の相対パスは、forge.yaml ファイルの親ディレクトリ基準で絶対化される。
|
|
10
|
+
これは alpha-forge の ``alpha_forge/config.py:_resolve_paths`` と同じ規約。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import pathlib
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import yaml
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class ForgeConfig:
|
|
25
|
+
"""forge プロジェクトのパス解決結果。
|
|
26
|
+
|
|
27
|
+
すべてのパスは ``from_forge_dir`` ファクトリで構築時に解決済みの絶対パスになる。
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
forge_dir: pathlib.Path
|
|
31
|
+
forge_db: pathlib.Path
|
|
32
|
+
strategies_dir: pathlib.Path
|
|
33
|
+
strategies_db: pathlib.Path | None
|
|
34
|
+
ideas_json: pathlib.Path
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_forge_dir(
|
|
38
|
+
cls,
|
|
39
|
+
forge_dir: pathlib.Path,
|
|
40
|
+
config_path: pathlib.Path | None = None,
|
|
41
|
+
) -> ForgeConfig:
|
|
42
|
+
"""forge.yaml を読み込んで設定を構築する。
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
forge_dir: forge プロジェクトのルートディレクトリ。
|
|
46
|
+
config_path: 明示指定する forge.yaml のパス(最優先)。
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
解決済みパスを持つ ``ForgeConfig`` インスタンス。
|
|
50
|
+
"""
|
|
51
|
+
forge_dir = pathlib.Path(forge_dir).resolve()
|
|
52
|
+
yaml_path = _resolve_yaml_path(forge_dir, config_path)
|
|
53
|
+
raw = _load_yaml(yaml_path) if yaml_path is not None else {}
|
|
54
|
+
|
|
55
|
+
# 相対パス解決の基準は forge.yaml の親ディレクトリ。
|
|
56
|
+
# forge.yaml が見つからない場合は forge_dir を基準にする(後方互換)。
|
|
57
|
+
base = yaml_path.parent if yaml_path is not None else forge_dir
|
|
58
|
+
|
|
59
|
+
report = raw.get("report") or {}
|
|
60
|
+
strategies = raw.get("strategies") or {}
|
|
61
|
+
ideas = raw.get("ideas") or {}
|
|
62
|
+
|
|
63
|
+
report_output = _resolve_path(
|
|
64
|
+
base, report.get("output_path"), default=forge_dir / "data" / "results"
|
|
65
|
+
)
|
|
66
|
+
forge_db_filename = report.get("db_filename") or "forge.db"
|
|
67
|
+
forge_db = report_output / forge_db_filename
|
|
68
|
+
|
|
69
|
+
strategies_path = _resolve_path(
|
|
70
|
+
base, strategies.get("path"), default=forge_dir / "data" / "strategies"
|
|
71
|
+
)
|
|
72
|
+
strategies_db: pathlib.Path | None = None
|
|
73
|
+
if bool(strategies.get("use_db", False)):
|
|
74
|
+
strategies_db_filename = strategies.get("db_filename") or "strategies.db"
|
|
75
|
+
strategies_db = strategies_path / strategies_db_filename
|
|
76
|
+
|
|
77
|
+
ideas_path = _resolve_path(
|
|
78
|
+
base, ideas.get("ideas_path"), default=forge_dir / "data" / "ideas"
|
|
79
|
+
)
|
|
80
|
+
ideas_json = ideas_path / "ideas.json"
|
|
81
|
+
|
|
82
|
+
return cls(
|
|
83
|
+
forge_dir=forge_dir,
|
|
84
|
+
forge_db=forge_db,
|
|
85
|
+
strategies_dir=strategies_path,
|
|
86
|
+
strategies_db=strategies_db,
|
|
87
|
+
ideas_json=ideas_json,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _resolve_yaml_path(
|
|
92
|
+
forge_dir: pathlib.Path, explicit: pathlib.Path | None
|
|
93
|
+
) -> pathlib.Path | None:
|
|
94
|
+
"""forge.yaml の最終的な探索結果を返す(見つからなければ None)。"""
|
|
95
|
+
if explicit is not None:
|
|
96
|
+
path = pathlib.Path(explicit).resolve()
|
|
97
|
+
return path if path.is_file() else None
|
|
98
|
+
|
|
99
|
+
env_value = os.environ.get("FORGE_CONFIG")
|
|
100
|
+
if env_value:
|
|
101
|
+
path = pathlib.Path(env_value).expanduser().resolve()
|
|
102
|
+
if path.is_file():
|
|
103
|
+
return path
|
|
104
|
+
|
|
105
|
+
candidate = (forge_dir / "forge.yaml").resolve()
|
|
106
|
+
return candidate if candidate.is_file() else None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _load_yaml(path: pathlib.Path) -> dict[str, Any]:
|
|
110
|
+
"""forge.yaml を安全にロードする。空ファイルや非辞書ルートは ``{}`` 扱い。"""
|
|
111
|
+
try:
|
|
112
|
+
text = path.read_text(encoding="utf-8")
|
|
113
|
+
except OSError:
|
|
114
|
+
return {}
|
|
115
|
+
raw = yaml.safe_load(text)
|
|
116
|
+
if isinstance(raw, dict):
|
|
117
|
+
return raw
|
|
118
|
+
return {}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _resolve_path(
|
|
122
|
+
base: pathlib.Path, value: Any, *, default: pathlib.Path
|
|
123
|
+
) -> pathlib.Path:
|
|
124
|
+
"""forge.yaml の相対パス文字列を ``base`` 基準で絶対化する。
|
|
125
|
+
|
|
126
|
+
値が ``None`` または空のときは ``default`` をそのまま返す。
|
|
127
|
+
"""
|
|
128
|
+
if value in (None, ""):
|
|
129
|
+
return default.resolve()
|
|
130
|
+
candidate = pathlib.Path(str(value)).expanduser()
|
|
131
|
+
if candidate.is_absolute():
|
|
132
|
+
return candidate.resolve()
|
|
133
|
+
return (base / candidate).resolve()
|
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""アイデア API ルーター
|
|
2
|
+
|
|
3
|
+
`/api/ideas` と `/api/ideas/{idea_id}` を提供する。
|
|
4
|
+
ForgeConfig.ideas_json から JSON を直接読み取る。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, HTTPException, Query, Request
|
|
13
|
+
|
|
14
|
+
from alpha_visualizer.forge_config import ForgeConfig
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
router = APIRouter()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load_ideas(config: ForgeConfig) -> list[dict[str, Any]]:
|
|
22
|
+
ideas_path = config.ideas_json
|
|
23
|
+
if not ideas_path.exists():
|
|
24
|
+
return []
|
|
25
|
+
try:
|
|
26
|
+
data = json.loads(ideas_path.read_text(encoding="utf-8"))
|
|
27
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
28
|
+
logger.warning("ideas.json の読み込みに失敗: %s", e)
|
|
29
|
+
return []
|
|
30
|
+
if isinstance(data, list):
|
|
31
|
+
return data
|
|
32
|
+
# {"ideas": [...]} 形式にも対応
|
|
33
|
+
if isinstance(data, dict):
|
|
34
|
+
return data.get("ideas", [])
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.get("/ideas")
|
|
39
|
+
async def list_ideas(
|
|
40
|
+
request: Request,
|
|
41
|
+
status: str | None = Query(default=None),
|
|
42
|
+
) -> list[dict[str, Any]]:
|
|
43
|
+
config: ForgeConfig = request.app.state.forge_config
|
|
44
|
+
ideas = _load_ideas(config)
|
|
45
|
+
if status is not None:
|
|
46
|
+
ideas = [i for i in ideas if isinstance(i, dict) and i.get("status") == status]
|
|
47
|
+
return ideas
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@router.get("/ideas/{idea_id}")
|
|
51
|
+
async def get_idea(idea_id: str, request: Request) -> dict[str, Any]:
|
|
52
|
+
config: ForgeConfig = request.app.state.forge_config
|
|
53
|
+
ideas = _load_ideas(config)
|
|
54
|
+
for idea in ideas:
|
|
55
|
+
if isinstance(idea, dict) and idea.get("idea_id") == idea_id:
|
|
56
|
+
return idea
|
|
57
|
+
raise HTTPException(status_code=404, detail=f"idea_id '{idea_id}' が見つかりません")
|