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 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)