alphafinch 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.
- alphafinch/__init__.py +0 -0
- alphafinch/braille.py +66 -0
- alphafinch/cinema.py +238 -0
- alphafinch/cli.py +455 -0
- alphafinch/data.py +438 -0
- alphafinch/engine.py +78 -0
- alphafinch/evolve.py +462 -0
- alphafinch/fitness.py +103 -0
- alphafinch/forward.py +96 -0
- alphafinch/lab.py +252 -0
- alphafinch/llm.py +146 -0
- alphafinch/mandate.py +131 -0
- alphafinch/prompts.py +164 -0
- alphafinch/report.py +188 -0
- alphafinch/sandbox.py +71 -0
- alphafinch/seeds.py +175 -0
- alphafinch/toolkit.py +140 -0
- alphafinch/ui.py +120 -0
- alphafinch/universe.py +114 -0
- alphafinch/universes/australia.csv +201 -0
- alphafinch/universes/canada.csv +61 -0
- alphafinch/universes/europe.csv +179 -0
- alphafinch/universes/hongkong.csv +86 -0
- alphafinch/universes/india_fno.csv +215 -0
- alphafinch/universes/japan.csv +226 -0
- alphafinch/universes/korea.csv +201 -0
- alphafinch/universes/uk.csv +101 -0
- alphafinch/world.py +110 -0
- alphafinch-0.1.0.dist-info/METADATA +255 -0
- alphafinch-0.1.0.dist-info/RECORD +33 -0
- alphafinch-0.1.0.dist-info/WHEEL +4 -0
- alphafinch-0.1.0.dist-info/entry_points.txt +2 -0
- alphafinch-0.1.0.dist-info/licenses/LICENSE +21 -0
alphafinch/__init__.py
ADDED
|
File without changes
|
alphafinch/braille.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Terminal line charts.
|
|
2
|
+
|
|
3
|
+
`chart` draws solid lines with box-drawing characters (asciichart style), which render
|
|
4
|
+
crisply in any monospace font. Several series share one y-axis; later series draw on top.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _resample(y: np.ndarray, n: int) -> np.ndarray:
|
|
13
|
+
"""Average within each column (smooths daily noise instead of sampling it)."""
|
|
14
|
+
if len(y) <= n:
|
|
15
|
+
idx = np.linspace(0, len(y) - 1, n)
|
|
16
|
+
return np.interp(idx, np.arange(len(y)), y)
|
|
17
|
+
edges = np.linspace(0, len(y), n + 1).astype(int)
|
|
18
|
+
return np.array([y[a:b].mean() for a, b in zip(edges[:-1], edges[1:])])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def chart(series: list[tuple[np.ndarray, str]], width: int = 56, height: int = 12,
|
|
22
|
+
upto: float = 1.0, log: bool = True) -> Text:
|
|
23
|
+
"""Render series (values, rich style) as solid lines. `upto` in (0, 1] draws only the
|
|
24
|
+
first fraction of each series, for animation."""
|
|
25
|
+
ys = []
|
|
26
|
+
for v, _ in series:
|
|
27
|
+
v = np.asarray(v, float)
|
|
28
|
+
ys.append(np.log(v) if log else v)
|
|
29
|
+
cols = max(2, width)
|
|
30
|
+
rs = [_resample(y, cols) for y in ys]
|
|
31
|
+
lo = min(float(r.min()) for r in rs)
|
|
32
|
+
hi = max(float(r.max()) for r in rs)
|
|
33
|
+
span = hi - lo if hi > lo else 1.0
|
|
34
|
+
grid = [[" "] * cols for _ in range(height)]
|
|
35
|
+
style = [[""] * cols for _ in range(height)]
|
|
36
|
+
last = max(2, int(cols * upto))
|
|
37
|
+
LIGHT = dict(h="─", up_from="╯", up_to="╭", down_from="╮", down_to="╰", v="│")
|
|
38
|
+
HEAVY = dict(h="━", up_from="┛", up_to="┏", down_from="┓", down_to="┗", v="┃")
|
|
39
|
+
for r, (_, st) in zip(rs, series):
|
|
40
|
+
g = HEAVY if "bold" in st else LIGHT
|
|
41
|
+
rows = np.round((1 - (r - lo) / span) * (height - 1)).astype(int)
|
|
42
|
+
for x in range(last):
|
|
43
|
+
y0 = rows[x]
|
|
44
|
+
if x == 0:
|
|
45
|
+
grid[y0][x], style[y0][x] = g["h"], st
|
|
46
|
+
continue
|
|
47
|
+
yp = rows[x - 1]
|
|
48
|
+
if y0 == yp:
|
|
49
|
+
grid[y0][x], style[y0][x] = g["h"], st
|
|
50
|
+
elif y0 < yp: # going up (smaller row index)
|
|
51
|
+
grid[yp][x], style[yp][x] = g["up_from"], st
|
|
52
|
+
grid[y0][x], style[y0][x] = g["up_to"], st
|
|
53
|
+
for k in range(y0 + 1, yp):
|
|
54
|
+
grid[k][x], style[k][x] = g["v"], st
|
|
55
|
+
else: # going down
|
|
56
|
+
grid[yp][x], style[yp][x] = g["down_from"], st
|
|
57
|
+
grid[y0][x], style[y0][x] = g["down_to"], st
|
|
58
|
+
for k in range(yp + 1, y0):
|
|
59
|
+
grid[k][x], style[k][x] = g["v"], st
|
|
60
|
+
out = Text()
|
|
61
|
+
for r in range(height):
|
|
62
|
+
for c in range(cols):
|
|
63
|
+
out.append(grid[r][c], style=style[r][c])
|
|
64
|
+
if r < height - 1:
|
|
65
|
+
out.append("\n")
|
|
66
|
+
return out
|
alphafinch/cinema.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""`alphafinch replay`: re-animate a finished run as a short cinematic story.
|
|
2
|
+
|
|
3
|
+
Every frame is reconstructed from the run's saved population (code, lineage, operators) and
|
|
4
|
+
from re-running the strategies on the same data, so nothing is invented. Scenes:
|
|
5
|
+
1. title card
|
|
6
|
+
2. births: the AI writing new strategies (changed lines highlighted) while their training
|
|
7
|
+
equity curves draw against their parent and the market; new champions are crowned
|
|
8
|
+
3. the sealed exam: the hidden years are unsealed and the verdict revealed
|
|
9
|
+
4. end card
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import difflib
|
|
14
|
+
import json
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pandas as pd
|
|
20
|
+
from rich.align import Align
|
|
21
|
+
from rich.console import Console, Group
|
|
22
|
+
from rich.live import Live
|
|
23
|
+
from rich.panel import Panel
|
|
24
|
+
from rich.table import Table
|
|
25
|
+
from rich.text import Text
|
|
26
|
+
|
|
27
|
+
from . import data, fitness
|
|
28
|
+
from .lab import Lab
|
|
29
|
+
from .braille import chart
|
|
30
|
+
from .ui import OP_ICON
|
|
31
|
+
|
|
32
|
+
OP_VERB = {"mutate": "mutating", "crossover": "crossing", "immigrant": "inventing", "tweak": "tuning",
|
|
33
|
+
"blend": "blending", "seed": "planting"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def exam_reveal(live, name, span, mkt_hold, rev, verdict, bar, width, sleep):
|
|
37
|
+
"""The dramatic ending: unseal the hidden years, draw the champion against the market,
|
|
38
|
+
stamp the verdict."""
|
|
39
|
+
for k in range(0, 21):
|
|
40
|
+
prog = "█" * k + "░" * (20 - k)
|
|
41
|
+
live.update(Panel(Group(Align.center(Text("🔒 THE SEALED EXAM", style="bold #f9e2af")),
|
|
42
|
+
Align.center(Text(f"{name} has never seen {span}", style="grey62")),
|
|
43
|
+
Text(""), Align.center(Text(f"unsealing {prog}", style="#f9e2af"))),
|
|
44
|
+
border_style="#f9e2af", width=width, padding=(1, 2)), refresh=True)
|
|
45
|
+
sleep(0.05)
|
|
46
|
+
curves = [(mkt_hold, "#6c7086"), (rev["equity"].values, "bold #a6e3a1")]
|
|
47
|
+
leg = Text()
|
|
48
|
+
for label, col in [("buy everything (equal weight)", "#6c7086"), (name, "bold #a6e3a1")]:
|
|
49
|
+
leg.append("━━ ", style=col)
|
|
50
|
+
leg.append(label + " ")
|
|
51
|
+
panel = None
|
|
52
|
+
for k in range(1, 36):
|
|
53
|
+
panel = Panel(Group(chart(curves, width=width - 6, height=12, upto=k / 35), Text(""), leg),
|
|
54
|
+
title=f"🔒 the sealed years, {span}: growth of $1", border_style="#f9e2af", width=width)
|
|
55
|
+
live.update(panel, refresh=True)
|
|
56
|
+
sleep(0.05)
|
|
57
|
+
grade = rev.get("grade", verdict)
|
|
58
|
+
passed = grade == "PASS"
|
|
59
|
+
label, colour = {"PASS": (" ✅ PASS ", "#1a7f37"), "PROMISING": (" 🤔 PROMISING ", "#9a6700"),
|
|
60
|
+
"FAIL": (" ❌ FAIL ", "#cf222e")}[grade]
|
|
61
|
+
stamp = Text.assemble((label, f"bold #ffffff on {colour}"),
|
|
62
|
+
(f" sealed-years alpha {rev['alpha']:+.1%}/yr · t = {rev['alpha_t']:.2f} · bar {bar:.2f}", "white"))
|
|
63
|
+
if passed:
|
|
64
|
+
moral = Text(" It beat the market by more than luck can explain.", style="bold #a6e3a1")
|
|
65
|
+
else:
|
|
66
|
+
mkt_cagr = mkt_hold[-1] ** (252 / max(len(mkt_hold) - 1, 1)) - 1
|
|
67
|
+
yn = rev.get("years_needed")
|
|
68
|
+
if grade == "PROMISING":
|
|
69
|
+
tail = f"Promising: an edge this size needs ~{yn:.0f} years of data to prove." if yn else "Promising, but unproven."
|
|
70
|
+
else:
|
|
71
|
+
tail = "No proof of a real edge."
|
|
72
|
+
moral = Text(f" It made {rev['cagr']:+.0%} a year; buying everything made {mkt_cagr:+.0%}. {tail}",
|
|
73
|
+
style="bold #f9e2af")
|
|
74
|
+
live.update(Group(panel, stamp, moral), refresh=True)
|
|
75
|
+
sleep(3.5)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Cinema:
|
|
79
|
+
def __init__(self, run_dir: Path, speed: float = 1.0, width: int = 118):
|
|
80
|
+
self.dir = Path(run_dir)
|
|
81
|
+
self.speed = speed
|
|
82
|
+
self.pop = json.loads((self.dir / "population.json").read_text())
|
|
83
|
+
self.by_id = {p["id"]: p for p in self.pop}
|
|
84
|
+
meta_f = self.dir / "meta.json"
|
|
85
|
+
self.meta = json.loads(meta_f.read_text()) if meta_f.exists() else {}
|
|
86
|
+
self.console = Console(width=width)
|
|
87
|
+
self.width = width
|
|
88
|
+
|
|
89
|
+
# helpers ---------------------------------------------------------------------------------
|
|
90
|
+
def _sleep(self, s):
|
|
91
|
+
time.sleep(s / self.speed)
|
|
92
|
+
|
|
93
|
+
def _equity(self, code):
|
|
94
|
+
r = self.lab.run(code, "train", check_leaks=False).returns
|
|
95
|
+
return (1 + r).cumprod().values
|
|
96
|
+
|
|
97
|
+
def _code_panel(self, ind, parent_code, shown_lines, title_extra=""):
|
|
98
|
+
lines = ind["code"].rstrip().splitlines()
|
|
99
|
+
new = set()
|
|
100
|
+
if parent_code:
|
|
101
|
+
pl = [l.strip() for l in parent_code.splitlines()]
|
|
102
|
+
new = {i for i, l in enumerate(lines) if l.strip() and l.strip() not in pl}
|
|
103
|
+
else:
|
|
104
|
+
new = set(range(len(lines)))
|
|
105
|
+
body = Text()
|
|
106
|
+
for i, l in enumerate(lines[:shown_lines][-16:]):
|
|
107
|
+
idx = i + max(0, shown_lines - 16)
|
|
108
|
+
mark, style = ("+ ", "bold green") if idx in new else (" ", "grey70")
|
|
109
|
+
body.append(mark, style="green" if idx in new else "grey35")
|
|
110
|
+
body.append(l[:60] + "\n", style=style)
|
|
111
|
+
if shown_lines < len(lines):
|
|
112
|
+
body.append("▌", style="bold white blink")
|
|
113
|
+
verb = OP_VERB.get(ind["op"], ind["op"])
|
|
114
|
+
parents = " + ".join(self.by_id[p]["name"] for p in ind["parents"] if p in self.by_id) or "a new idea"
|
|
115
|
+
return Panel(body, title=f"{OP_ICON.get(ind['op'], '•')} the AI is {verb}: [bold]{ind['name']}[/]{title_extra}",
|
|
116
|
+
subtitle=f"[grey62]from {parents}[/]", border_style="magenta", height=20, width=68)
|
|
117
|
+
|
|
118
|
+
def _chart_panel(self, curves, upto, title, legend):
|
|
119
|
+
c = chart(curves, width=self.width - 72, height=15, upto=upto)
|
|
120
|
+
leg = Text()
|
|
121
|
+
for name, colour in legend:
|
|
122
|
+
leg.append("━━ ", style=colour)
|
|
123
|
+
leg.append(name + " ", style="white")
|
|
124
|
+
return Panel(Group(c, Text(""), leg), title=title, border_style="cyan", height=20, width=self.width - 68)
|
|
125
|
+
|
|
126
|
+
def _header(self, gen, bred, champ_name, champ_fit):
|
|
127
|
+
t = Text.assemble(("🐦 AlphaFinch ", "bold magenta"),
|
|
128
|
+
(f"{self.meta.get('market_label', 'market')} ", "bold"),
|
|
129
|
+
(f"generation {gen} ", "cyan"), (f"strategies bred {bred} ", "white"),
|
|
130
|
+
("👑 ", ""), (f"{champ_name} ", "bold green"), (f"{champ_fit:+.2f}", "green"))
|
|
131
|
+
return Panel(t, border_style="magenta", width=self.width)
|
|
132
|
+
|
|
133
|
+
# scenes ----------------------------------------------------------------------------------
|
|
134
|
+
def _prepare(self):
|
|
135
|
+
market = self.meta.get("market", "us")
|
|
136
|
+
tickers = self.meta.get("tickers")
|
|
137
|
+
px = data.load(market, tickers.split(",") if tickers else None, self.meta.get("start"))
|
|
138
|
+
hold = pd.Timestamp(self.meta["holdout_start"])
|
|
139
|
+
self.lab = Lab(px, hold, workers=2)
|
|
140
|
+
self.mkt_train = (1 + self.lab.mkt["train"]).cumprod().values
|
|
141
|
+
ordered = sorted([p for p in self.pop if p["fitness"] is not None], key=lambda p: p["id"])
|
|
142
|
+
self.seeds = [p for p in ordered if p["op"] == "seed"]
|
|
143
|
+
self.children = [p for p in ordered if p["op"] not in ("seed", "migrant")]
|
|
144
|
+
self.stillborn = [p for p in self.pop if p["error"]]
|
|
145
|
+
best, rec = max(s["fitness"] for s in self.seeds), []
|
|
146
|
+
ai_ops = ("mutate", "crossover", "immigrant")
|
|
147
|
+
for c in self.children: # every new record holder
|
|
148
|
+
if c["fitness"] > best + 1e-9:
|
|
149
|
+
best = c["fitness"]
|
|
150
|
+
rec.append(c["id"])
|
|
151
|
+
ai_rec = [r for r in rec if self.by_id[r]["op"] in ai_ops]
|
|
152
|
+
self.featured = (ai_rec or rec)[-3:]
|
|
153
|
+
by_id = {p["id"]: p for p in ordered}
|
|
154
|
+
self.champ = by_id.get(self.meta.get("champion_id")) or max(ordered, key=lambda p: p["fitness"])
|
|
155
|
+
self.eq = {}
|
|
156
|
+
for cid in self.featured:
|
|
157
|
+
c = self.by_id[cid]
|
|
158
|
+
par = self.by_id.get(c["parents"][0]) if c["parents"] else None
|
|
159
|
+
self.eq[cid] = self._equity(c["code"])
|
|
160
|
+
if par:
|
|
161
|
+
self.eq[par["id"]] = self._equity(par["code"])
|
|
162
|
+
self.exam = fitness.SealedExam(self.lab, budget=self.meta.get("exam_budget", 10),
|
|
163
|
+
alpha=self.meta.get("alpha", 0.05))
|
|
164
|
+
self.rev = self.exam.reveal(self.champ["id"], self.champ["code"])
|
|
165
|
+
self.mkt_hold = self.rev["market_equity"].values
|
|
166
|
+
self.lab.close()
|
|
167
|
+
self.span = f"{hold.strftime('%b %Y')} – {px.index[-1].strftime('%b %Y')}"
|
|
168
|
+
v = next((e["verdict"] for e in self.meta.get("exam", []) if e["id"] == self.champ["id"]), None)
|
|
169
|
+
self.verdict = v or ("PASS" if self.rev["alpha_t"] > self.exam.bar else "FAIL")
|
|
170
|
+
|
|
171
|
+
def run(self):
|
|
172
|
+
with self.console.status("[magenta]⏪ rewinding the run…"):
|
|
173
|
+
self._prepare()
|
|
174
|
+
islands = self.meta.get("islands", 4)
|
|
175
|
+
model = self.meta.get("model") or self.meta.get("provider") or "AI"
|
|
176
|
+
model = {"sonnet": "Claude Sonnet", "opus": "Claude Opus"}.get(model, model)
|
|
177
|
+
feed: list[Text] = []
|
|
178
|
+
|
|
179
|
+
def frame(*parts):
|
|
180
|
+
return Group(*parts)
|
|
181
|
+
|
|
182
|
+
with Live(console=self.console, auto_refresh=False, screen=False, transient=False) as live:
|
|
183
|
+
# 1. title ---------------------------------------------------------------------------
|
|
184
|
+
title = Group(Align.center(Text("🐦 AlphaFinch", style="bold magenta")),
|
|
185
|
+
Align.center(Text("AI breeds trading strategies. A sealed exam decides if they're real.")),
|
|
186
|
+
Text(""),
|
|
187
|
+
Align.center(Text(f"{self.meta.get('market_label', '')} · {islands} islands · bred by {model}",
|
|
188
|
+
style="grey62")))
|
|
189
|
+
live.update(Panel(title, border_style="magenta", width=self.width, padding=(1, 2)), refresh=True)
|
|
190
|
+
self._sleep(2.0)
|
|
191
|
+
|
|
192
|
+
# 2. births --------------------------------------------------------------------------
|
|
193
|
+
cur = max(self.seeds, key=lambda p: p["fitness"])
|
|
194
|
+
bred = 0
|
|
195
|
+
for c in self.children:
|
|
196
|
+
bred += 1
|
|
197
|
+
line = Text.assemble((f" {OP_ICON.get(c['op'], '•')} ", ""), (f"{c['name']:<30}", "white"),
|
|
198
|
+
(f"{c['op']:<10}", "grey50"), (f"fitness {c['fitness']:+.2f}", "grey62"))
|
|
199
|
+
if c["id"] not in self.featured:
|
|
200
|
+
feed.append(line)
|
|
201
|
+
live.update(frame(self._header(c["gen"], bred, cur["name"], cur["fitness"]),
|
|
202
|
+
Panel(Group(*feed[-12:]), title="🐣 strategies being born", border_style="grey35",
|
|
203
|
+
width=self.width, height=14)), refresh=True)
|
|
204
|
+
self._sleep(0.09)
|
|
205
|
+
continue
|
|
206
|
+
par = self.by_id.get(c["parents"][0]) if c["parents"] else None
|
|
207
|
+
curves = [(self.mkt_train, "#6c7086")] + ([(self.eq[par["id"]], "#89b4fa")] if par else []) + [(self.eq[c["id"]], "bold #a6e3a1")]
|
|
208
|
+
legend = [("market", "#6c7086")] + ([("parent", "#89b4fa")] if par else []) + [(c["name"][:22], "bold #a6e3a1")]
|
|
209
|
+
n_lines = len(c["code"].splitlines())
|
|
210
|
+
steps = 30
|
|
211
|
+
row = None
|
|
212
|
+
for k in range(1, steps + 1):
|
|
213
|
+
shown = int(n_lines * min(1, k / (steps * 0.55)))
|
|
214
|
+
upto = min(1.0, max(0.03, (k - steps * 0.3) / (steps * 0.6)))
|
|
215
|
+
row = Table.grid()
|
|
216
|
+
row.add_row(self._code_panel(c, par["code"] if par else None, shown),
|
|
217
|
+
self._chart_panel(curves, upto, "📈 growth of $1 (training years)", legend))
|
|
218
|
+
live.update(frame(self._header(c["gen"], bred, cur["name"], cur["fitness"]), row), refresh=True)
|
|
219
|
+
self._sleep(0.06)
|
|
220
|
+
beat = f" beats {par['name']} ({par['fitness']:+.2f})" if par else ""
|
|
221
|
+
crown = Text.assemble((" 👑 NEW CHAMPION ", "bold #1e1e2e on #f9e2af"), (" ", ""), (c["name"], "bold #f9e2af"),
|
|
222
|
+
(f" fitness {c['fitness']:+.2f}{beat}", "#f9e2af"))
|
|
223
|
+
cur = c
|
|
224
|
+
live.update(frame(self._header(c["gen"], bred, cur["name"], cur["fitness"]), row, crown), refresh=True)
|
|
225
|
+
self._sleep(1.8)
|
|
226
|
+
feed.append(Text.assemble((" 👑 ", ""), (f"{c['name']:<30}", "bold #f9e2af"), (f"{c['op']:<10}", "grey50"),
|
|
227
|
+
(f"fitness {c['fitness']:+.2f}", "#f9e2af")))
|
|
228
|
+
|
|
229
|
+
# 3. the sealed exam -------------------------------------------------------------------
|
|
230
|
+
exam_reveal(live, self.champ["name"], self.span, self.mkt_hold, self.rev, self.verdict,
|
|
231
|
+
self.exam.bar, self.width, self._sleep)
|
|
232
|
+
|
|
233
|
+
# 4. end card ------------------------------------------------------------------------
|
|
234
|
+
live.update(Panel(Group(Align.center(Text("🐦 AlphaFinch", style="bold magenta")),
|
|
235
|
+
Align.center(Text("evolve strategies while you sleep · an exam they can't cheat")),
|
|
236
|
+
Text(""), Align.center(Text("pip install alphafinch", style="bold cyan"))),
|
|
237
|
+
border_style="magenta", width=self.width, padding=(1, 2)), refresh=True)
|
|
238
|
+
self._sleep(2.5)
|