Spaces:
Sleeping
Sleeping
Add Pyright configuration, enhance dataset building, and update training notebook
Browse files- Created a new Pyright configuration file for type checking.
- Improved error handling in `fetch_ohlcv` function to check for None values.
- Refactored `build_prices` function for better clarity and type safety.
- Updated `build_indicators` to ensure proper type handling for price data.
- Enhanced the training notebook with clearer markdown and structured code cells.
- Added new evaluation and summary files for training runs.
- Included binary files for portfolio and reward curves in evaluation runs.
- app/council/llm.py +28 -3
- app/data/indicators.py +8 -3
- app/data/loader.py +3 -1
- pyrightconfig.json +9 -0
- scripts/build_dataset.py +28 -24
- training/runs/eval_smoke/portfolio_curve.png +0 -0
- training/runs/eval_smoke/reward_curve.png +0 -0
- training/runs/eval_smoke/summary.csv +4 -0
- training/train_grpo.ipynb +348 -26
app/council/llm.py
CHANGED
|
@@ -180,17 +180,29 @@ class TransformersLLMClient:
|
|
| 180 |
cls,
|
| 181 |
model_id: str = "google/gemma-4-E4B-it",
|
| 182 |
load_in_4bit: bool = True,
|
| 183 |
-
device_map:
|
| 184 |
) -> "TransformersLLMClient":
|
|
|
|
| 185 |
from transformers import AutoModelForCausalLM, AutoProcessor
|
| 186 |
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
if load_in_4bit:
|
| 189 |
from transformers import BitsAndBytesConfig
|
| 190 |
kwargs["quantization_config"] = BitsAndBytesConfig(
|
| 191 |
load_in_4bit=True,
|
| 192 |
-
bnb_4bit_compute_dtype=
|
| 193 |
bnb_4bit_quant_type="nf4",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
)
|
| 195 |
processor = AutoProcessor.from_pretrained(model_id)
|
| 196 |
model = AutoModelForCausalLM.from_pretrained(model_id, **kwargs)
|
|
@@ -244,6 +256,15 @@ class TransformersLLMClient:
|
|
| 244 |
# ----------------------------------------------------------------- helpers
|
| 245 |
@staticmethod
|
| 246 |
def _prep_messages(messages: list[dict]) -> list[dict]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
import base64
|
| 248 |
from io import BytesIO
|
| 249 |
|
|
@@ -252,6 +273,10 @@ class TransformersLLMClient:
|
|
| 252 |
out = []
|
| 253 |
for m in messages:
|
| 254 |
content = m.get("content")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
if isinstance(content, list):
|
| 256 |
new_parts = []
|
| 257 |
for part in content:
|
|
|
|
| 180 |
cls,
|
| 181 |
model_id: str = "google/gemma-4-E4B-it",
|
| 182 |
load_in_4bit: bool = True,
|
| 183 |
+
device_map: Any = "auto",
|
| 184 |
) -> "TransformersLLMClient":
|
| 185 |
+
import torch
|
| 186 |
from transformers import AutoModelForCausalLM, AutoProcessor
|
| 187 |
|
| 188 |
+
# T4 (Turing) doesn't have bf16 hardware — fall back to fp16 there.
|
| 189 |
+
compute_dtype = (
|
| 190 |
+
"bfloat16"
|
| 191 |
+
if (torch.cuda.is_available() and torch.cuda.is_bf16_supported())
|
| 192 |
+
else "float16"
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
kwargs: dict = {"dtype": compute_dtype, "device_map": device_map}
|
| 196 |
if load_in_4bit:
|
| 197 |
from transformers import BitsAndBytesConfig
|
| 198 |
kwargs["quantization_config"] = BitsAndBytesConfig(
|
| 199 |
load_in_4bit=True,
|
| 200 |
+
bnb_4bit_compute_dtype=compute_dtype,
|
| 201 |
bnb_4bit_quant_type="nf4",
|
| 202 |
+
bnb_4bit_use_double_quant=True,
|
| 203 |
+
# Allow non-quantizable modules (vision tower, embeddings) to
|
| 204 |
+
# land on CPU when GPU is tight — required on T4 for E4B.
|
| 205 |
+
llm_int8_enable_fp32_cpu_offload=True,
|
| 206 |
)
|
| 207 |
processor = AutoProcessor.from_pretrained(model_id)
|
| 208 |
model = AutoModelForCausalLM.from_pretrained(model_id, **kwargs)
|
|
|
|
| 256 |
# ----------------------------------------------------------------- helpers
|
| 257 |
@staticmethod
|
| 258 |
def _prep_messages(messages: list[dict]) -> list[dict]:
|
| 259 |
+
"""Normalize OpenAI-style messages for HF AutoProcessor chat templates.
|
| 260 |
+
|
| 261 |
+
Two transforms:
|
| 262 |
+
1. Plain string `content` is wrapped as `[{"type": "text", "text": ...}]`
|
| 263 |
+
because multimodal processors iterate `content` and break on strings.
|
| 264 |
+
2. `{"type": "image_url", "image_url": {"url": "data:image/..."}}` parts
|
| 265 |
+
are decoded back to PIL.Image objects in `{"type": "image", "image": img}`
|
| 266 |
+
form expected by Gemma's processor.
|
| 267 |
+
"""
|
| 268 |
import base64
|
| 269 |
from io import BytesIO
|
| 270 |
|
|
|
|
| 273 |
out = []
|
| 274 |
for m in messages:
|
| 275 |
content = m.get("content")
|
| 276 |
+
if isinstance(content, str):
|
| 277 |
+
# Plain string -> single text part
|
| 278 |
+
out.append({**m, "content": [{"type": "text", "text": content}]})
|
| 279 |
+
continue
|
| 280 |
if isinstance(content, list):
|
| 281 |
new_parts = []
|
| 282 |
for part in content:
|
app/data/indicators.py
CHANGED
|
@@ -17,7 +17,7 @@ def rsi(close: pd.Series, length: int = 14) -> pd.Series:
|
|
| 17 |
avg_gain = gain.ewm(alpha=1 / length, adjust=False, min_periods=length).mean()
|
| 18 |
avg_loss = loss.ewm(alpha=1 / length, adjust=False, min_periods=length).mean()
|
| 19 |
rs = avg_gain / avg_loss.replace(0, float("nan"))
|
| 20 |
-
return 100 - (100 / (1 + rs))
|
| 21 |
|
| 22 |
|
| 23 |
def macd(
|
|
@@ -33,7 +33,9 @@ def macd(
|
|
| 33 |
|
| 34 |
|
| 35 |
def sma(close: pd.Series, length: int) -> pd.Series:
|
| 36 |
-
return
|
|
|
|
|
|
|
| 37 |
|
| 38 |
|
| 39 |
def bbands(close: pd.Series, length: int = 20, std: float = 2.0) -> pd.DataFrame:
|
|
@@ -55,4 +57,7 @@ def atr(
|
|
| 55 |
],
|
| 56 |
axis=1,
|
| 57 |
).max(axis=1)
|
| 58 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
avg_gain = gain.ewm(alpha=1 / length, adjust=False, min_periods=length).mean()
|
| 18 |
avg_loss = loss.ewm(alpha=1 / length, adjust=False, min_periods=length).mean()
|
| 19 |
rs = avg_gain / avg_loss.replace(0, float("nan"))
|
| 20 |
+
return pd.Series(100 - (100 / (1 + rs)), index=close.index, name="rsi")
|
| 21 |
|
| 22 |
|
| 23 |
def macd(
|
|
|
|
| 33 |
|
| 34 |
|
| 35 |
def sma(close: pd.Series, length: int) -> pd.Series:
|
| 36 |
+
return pd.Series(
|
| 37 |
+
close.rolling(length, min_periods=length).mean(), index=close.index
|
| 38 |
+
)
|
| 39 |
|
| 40 |
|
| 41 |
def bbands(close: pd.Series, length: int = 20, std: float = 2.0) -> pd.DataFrame:
|
|
|
|
| 57 |
],
|
| 58 |
axis=1,
|
| 59 |
).max(axis=1)
|
| 60 |
+
return pd.Series(
|
| 61 |
+
tr.ewm(alpha=1 / length, adjust=False, min_periods=length).mean(),
|
| 62 |
+
index=close.index,
|
| 63 |
+
)
|
app/data/loader.py
CHANGED
|
@@ -57,7 +57,9 @@ def macro() -> pd.DataFrame:
|
|
| 57 |
def episode_rows(task_id: str) -> pd.DataFrame:
|
| 58 |
"""Episode prices for a task, sorted by date."""
|
| 59 |
df = prices()
|
| 60 |
-
|
|
|
|
|
|
|
| 61 |
|
| 62 |
|
| 63 |
def lookup_indicators(ticker: str, date: str) -> dict:
|
|
|
|
| 57 |
def episode_rows(task_id: str) -> pd.DataFrame:
|
| 58 |
"""Episode prices for a task, sorted by date."""
|
| 59 |
df = prices()
|
| 60 |
+
mask = (df["task_id"] == task_id) & df["in_episode"]
|
| 61 |
+
sub = df.loc[mask]
|
| 62 |
+
return sub.sort_values(by="date").reset_index(drop=True)
|
| 63 |
|
| 64 |
|
| 65 |
def lookup_indicators(ticker: str, date: str) -> dict:
|
pyrightconfig.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"include": ["app", "scripts", "server", "tests", "training", "inference.py", "client.py"],
|
| 3 |
+
"exclude": ["**/__pycache__", "**/.venv", "**/.cache", "training/runs"],
|
| 4 |
+
"extraPaths": ["."],
|
| 5 |
+
"typeCheckingMode": "basic",
|
| 6 |
+
"reportMissingImports": "warning",
|
| 7 |
+
"reportMissingTypeStubs": "none",
|
| 8 |
+
"pythonVersion": "3.12"
|
| 9 |
+
}
|
scripts/build_dataset.py
CHANGED
|
@@ -82,12 +82,14 @@ def fetch_ohlcv(ticker: str, start: str, end: str) -> pd.DataFrame:
|
|
| 82 |
df = yf.download(
|
| 83 |
ticker, start=start, end=end, auto_adjust=False, progress=False
|
| 84 |
)
|
| 85 |
-
if df.empty:
|
| 86 |
raise RuntimeError(f"yfinance returned empty for {ticker} {start}..{end}")
|
| 87 |
if isinstance(df.columns, pd.MultiIndex):
|
| 88 |
df.columns = df.columns.get_level_values(0)
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
| 91 |
df.columns = ["open", "high", "low", "close", "volume"]
|
| 92 |
return df
|
| 93 |
|
|
@@ -97,38 +99,39 @@ def lookback_start(episode_start: str, days: int) -> str:
|
|
| 97 |
|
| 98 |
|
| 99 |
def build_prices() -> pd.DataFrame:
|
| 100 |
-
rows = []
|
| 101 |
for task in TASKS:
|
| 102 |
start = lookback_start(task["episode_start"], LOOKBACK_DAYS)
|
| 103 |
end = (pd.to_datetime(task["episode_end"]) + pd.Timedelta(days=1)).strftime("%Y-%m-%d")
|
| 104 |
df = fetch_ohlcv(task["ticker"], start, end)
|
|
|
|
|
|
|
| 105 |
for date, row in df.iterrows():
|
|
|
|
| 106 |
rows.append({
|
| 107 |
"task_id": task["task_id"],
|
| 108 |
"ticker": task["ticker"],
|
| 109 |
-
"date":
|
| 110 |
-
"open":
|
| 111 |
-
"high":
|
| 112 |
-
"low":
|
| 113 |
-
"close":
|
| 114 |
-
"volume": float(row["volume"]),
|
| 115 |
-
"in_episode":
|
| 116 |
-
pd.to_datetime(task["episode_start"])
|
| 117 |
-
<= date
|
| 118 |
-
<= pd.to_datetime(task["episode_end"])
|
| 119 |
-
),
|
| 120 |
})
|
| 121 |
return pd.DataFrame(rows)
|
| 122 |
|
| 123 |
|
| 124 |
def build_indicators(prices: pd.DataFrame) -> pd.DataFrame:
|
| 125 |
"""Compute indicators per ticker on the full lookback+episode window."""
|
| 126 |
-
out_rows = []
|
| 127 |
for ticker, group in prices.groupby("ticker"):
|
| 128 |
-
df = group.sort_values("date").set_index(
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
| 132 |
|
| 133 |
ind = pd.DataFrame(index=df.index)
|
| 134 |
ind["rsi14"] = ta.rsi(close, length=14)
|
|
@@ -147,10 +150,10 @@ def build_indicators(prices: pd.DataFrame) -> pd.DataFrame:
|
|
| 147 |
ind["ticker"] = ticker
|
| 148 |
ind["date"] = ind["date"].dt.strftime("%Y-%m-%d")
|
| 149 |
out_rows.append(ind)
|
| 150 |
-
out = pd.concat(out_rows, ignore_index=True)
|
| 151 |
cols = ["ticker", "date", "rsi14", "macd", "macd_signal",
|
| 152 |
"sma20", "sma50", "sma200", "bb_lower", "bb_upper", "atr14"]
|
| 153 |
-
return out[cols]
|
| 154 |
|
| 155 |
|
| 156 |
def build_peers() -> pd.DataFrame:
|
|
@@ -232,8 +235,9 @@ def render_charts(prices: pd.DataFrame) -> int:
|
|
| 232 |
for ticker, group in prices.groupby("ticker"):
|
| 233 |
group = group.sort_values("date").reset_index(drop=True)
|
| 234 |
group["date_dt"] = pd.to_datetime(group["date"])
|
| 235 |
-
for
|
| 236 |
-
|
|
|
|
| 237 |
continue
|
| 238 |
window_start_idx = max(0, i - 60)
|
| 239 |
window = group.iloc[window_start_idx : i + 1].copy()
|
|
|
|
| 82 |
df = yf.download(
|
| 83 |
ticker, start=start, end=end, auto_adjust=False, progress=False
|
| 84 |
)
|
| 85 |
+
if df is None or df.empty:
|
| 86 |
raise RuntimeError(f"yfinance returned empty for {ticker} {start}..{end}")
|
| 87 |
if isinstance(df.columns, pd.MultiIndex):
|
| 88 |
df.columns = df.columns.get_level_values(0)
|
| 89 |
+
idx = pd.DatetimeIndex(pd.to_datetime(df.index)).tz_localize(None)
|
| 90 |
+
idx = idx.normalize() # type: ignore[attr-defined]
|
| 91 |
+
df.index = idx
|
| 92 |
+
df = df.loc[:, ["Open", "High", "Low", "Close", "Volume"]]
|
| 93 |
df.columns = ["open", "high", "low", "close", "volume"]
|
| 94 |
return df
|
| 95 |
|
|
|
|
| 99 |
|
| 100 |
|
| 101 |
def build_prices() -> pd.DataFrame:
|
| 102 |
+
rows: list[dict] = []
|
| 103 |
for task in TASKS:
|
| 104 |
start = lookback_start(task["episode_start"], LOOKBACK_DAYS)
|
| 105 |
end = (pd.to_datetime(task["episode_end"]) + pd.Timedelta(days=1)).strftime("%Y-%m-%d")
|
| 106 |
df = fetch_ohlcv(task["ticker"], start, end)
|
| 107 |
+
episode_start = pd.to_datetime(task["episode_start"])
|
| 108 |
+
episode_end = pd.to_datetime(task["episode_end"])
|
| 109 |
for date, row in df.iterrows():
|
| 110 |
+
ts = pd.Timestamp(date) # type: ignore[arg-type]
|
| 111 |
rows.append({
|
| 112 |
"task_id": task["task_id"],
|
| 113 |
"ticker": task["ticker"],
|
| 114 |
+
"date": ts.strftime("%Y-%m-%d"),
|
| 115 |
+
"open": float(row["open"]), # type: ignore[arg-type]
|
| 116 |
+
"high": float(row["high"]), # type: ignore[arg-type]
|
| 117 |
+
"low": float(row["low"]), # type: ignore[arg-type]
|
| 118 |
+
"close": float(row["close"]), # type: ignore[arg-type]
|
| 119 |
+
"volume": float(row["volume"]), # type: ignore[arg-type]
|
| 120 |
+
"in_episode": episode_start <= ts <= episode_end,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
})
|
| 122 |
return pd.DataFrame(rows)
|
| 123 |
|
| 124 |
|
| 125 |
def build_indicators(prices: pd.DataFrame) -> pd.DataFrame:
|
| 126 |
"""Compute indicators per ticker on the full lookback+episode window."""
|
| 127 |
+
out_rows: list[pd.DataFrame] = []
|
| 128 |
for ticker, group in prices.groupby("ticker"):
|
| 129 |
+
df = group.sort_values("date").set_index(
|
| 130 |
+
pd.to_datetime(group["date"])
|
| 131 |
+
)
|
| 132 |
+
close = pd.Series(df["close"].astype(float))
|
| 133 |
+
high = pd.Series(df["high"].astype(float))
|
| 134 |
+
low = pd.Series(df["low"].astype(float))
|
| 135 |
|
| 136 |
ind = pd.DataFrame(index=df.index)
|
| 137 |
ind["rsi14"] = ta.rsi(close, length=14)
|
|
|
|
| 150 |
ind["ticker"] = ticker
|
| 151 |
ind["date"] = ind["date"].dt.strftime("%Y-%m-%d")
|
| 152 |
out_rows.append(ind)
|
| 153 |
+
out: pd.DataFrame = pd.concat(out_rows, ignore_index=True)
|
| 154 |
cols = ["ticker", "date", "rsi14", "macd", "macd_signal",
|
| 155 |
"sma20", "sma50", "sma200", "bb_lower", "bb_upper", "atr14"]
|
| 156 |
+
return out.loc[:, cols]
|
| 157 |
|
| 158 |
|
| 159 |
def build_peers() -> pd.DataFrame:
|
|
|
|
| 235 |
for ticker, group in prices.groupby("ticker"):
|
| 236 |
group = group.sort_values("date").reset_index(drop=True)
|
| 237 |
group["date_dt"] = pd.to_datetime(group["date"])
|
| 238 |
+
for raw_i, row in group.iterrows():
|
| 239 |
+
i = int(raw_i) # type: ignore[arg-type]
|
| 240 |
+
if not bool(row["in_episode"]):
|
| 241 |
continue
|
| 242 |
window_start_idx = max(0, i - 60)
|
| 243 |
window = group.iloc[window_start_idx : i + 1].copy()
|
training/runs/eval_smoke/portfolio_curve.png
ADDED
|
training/runs/eval_smoke/reward_curve.png
ADDED
|
training/runs/eval_smoke/summary.csv
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
task_id,total_reward,final_portfolio,buy_and_hold,alpha_pct
|
| 2 |
+
task_easy,0.1,10000.0,8755.6,14.21
|
| 3 |
+
task_medium,0.1,10000.0,9007.25,11.02
|
| 4 |
+
task_hard,0.1,10000.0,5668.0,76.43
|
training/train_grpo.ipynb
CHANGED
|
@@ -4,23 +4,82 @@
|
|
| 4 |
"cell_type": "markdown",
|
| 5 |
"id": "intro",
|
| 6 |
"metadata": {},
|
| 7 |
-
"source":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
},
|
| 9 |
{
|
| 10 |
"cell_type": "code",
|
| 11 |
-
"execution_count":
|
| 12 |
"id": "install",
|
| 13 |
"metadata": {},
|
| 14 |
"outputs": [],
|
| 15 |
-
"source":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
},
|
| 17 |
{
|
| 18 |
"cell_type": "code",
|
| 19 |
-
"execution_count":
|
| 20 |
"id": "clone",
|
| 21 |
"metadata": {},
|
| 22 |
-
"outputs": [
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
},
|
| 25 |
{
|
| 26 |
"cell_type": "code",
|
|
@@ -28,39 +87,253 @@
|
|
| 28 |
"id": "auth",
|
| 29 |
"metadata": {},
|
| 30 |
"outputs": [],
|
| 31 |
-
"source":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
},
|
| 33 |
{
|
| 34 |
"cell_type": "code",
|
| 35 |
"execution_count": null,
|
| 36 |
"id": "build-data",
|
| 37 |
"metadata": {},
|
| 38 |
-
"outputs": [
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
},
|
| 41 |
{
|
| 42 |
"cell_type": "code",
|
|
|
|
| 43 |
"id": "f55a1108",
|
| 44 |
-
"source": "from app.council.llm import TransformersLLMClient\n\n# 4-bit BnB by default; drop load_in_4bit=False on L4/A100.\nclient = TransformersLLMClient.from_pretrained(\n \"google/gemma-4-E4B-it\",\n load_in_4bit=True,\n)\nprint(\"Model loaded on:\", client.model.device)",
|
| 45 |
"metadata": {},
|
| 46 |
-
"
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
},
|
| 49 |
{
|
| 50 |
"cell_type": "code",
|
|
|
|
| 51 |
"id": "a3a9bd4c",
|
| 52 |
-
"source": "# Pre-cache the 7 specialists' votes for every (task, step). Specialists\n# are FROZEN, so this runs once and is reused across every GRPO step.\nfrom app.council.runner import Council\nfrom app.core.environment import StockerEnv\nfrom app.core.tasks import list_task_ids\n\ncouncil = Council(client=client, use_cache=True)\ntotal = 0\nfor task_id in list_task_ids():\n env = StockerEnv(task_id=task_id)\n obs = env.reset().observation\n while True:\n for sp in council.specialists:\n council._cached_vote(sp, obs) # writes .cache/council/<role>/...\n total += 1\n result = env.step({\"side\": \"hold\", \"quantity\": 0})\n if result.done:\n break\n obs = result.observation\nprint(f\"cached {total} specialist votes across {len(list_task_ids())} tasks\")",
|
| 53 |
"metadata": {},
|
| 54 |
-
"
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
},
|
| 57 |
{
|
| 58 |
"cell_type": "code",
|
|
|
|
| 59 |
"id": "6af95f46",
|
| 60 |
-
"source": "# Pre-training baseline rollout (specialists from cache, moderator = base Gemma)\n!python -m training.eval_rollout --out training/runs/eval_pre\n!cat training/runs/eval_pre/summary.csv",
|
| 61 |
"metadata": {},
|
| 62 |
-
"
|
| 63 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
},
|
| 65 |
{
|
| 66 |
"cell_type": "code",
|
|
@@ -68,7 +341,20 @@
|
|
| 68 |
"id": "train",
|
| 69 |
"metadata": {},
|
| 70 |
"outputs": [],
|
| 71 |
-
"source":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
},
|
| 73 |
{
|
| 74 |
"cell_type": "code",
|
|
@@ -76,27 +362,63 @@
|
|
| 76 |
"id": "eval",
|
| 77 |
"metadata": {},
|
| 78 |
"outputs": [],
|
| 79 |
-
"source":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
},
|
| 81 |
{
|
| 82 |
"cell_type": "code",
|
|
|
|
| 83 |
"id": "b1d888ed",
|
| 84 |
-
"source": "# Show training + eval curves inline\nfrom IPython.display import Image, display\nimport os\nfor p in [f\"{RUN_DIR}/loss.png\", f\"{RUN_DIR}/reward.png\",\n \"training/runs/eval_pre/reward_curve.png\",\n \"training/runs/eval_post/reward_curve.png\",\n \"training/runs/eval_pre/portfolio_curve.png\",\n \"training/runs/eval_post/portfolio_curve.png\"]:\n if os.path.exists(p):\n print(p)\n display(Image(p))",
|
| 85 |
"metadata": {},
|
| 86 |
-
"
|
| 87 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
}
|
| 89 |
],
|
| 90 |
"metadata": {
|
| 91 |
"kernelspec": {
|
| 92 |
-
"display_name": "Python 3",
|
| 93 |
"language": "python",
|
| 94 |
"name": "python3"
|
| 95 |
},
|
| 96 |
"language_info": {
|
| 97 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
}
|
| 99 |
},
|
| 100 |
"nbformat": 4,
|
| 101 |
"nbformat_minor": 5
|
| 102 |
-
}
|
|
|
|
| 4 |
"cell_type": "markdown",
|
| 5 |
"id": "intro",
|
| 6 |
"metadata": {},
|
| 7 |
+
"source": [
|
| 8 |
+
"# Stocker — End-to-end Colab workflow\n",
|
| 9 |
+
"\n",
|
| 10 |
+
"Self-contained: clones the repo, installs deps, builds the dataset, loads\n",
|
| 11 |
+
"**`google/gemma-4-E4B-it`** in-process via `transformers` (4-bit BnB),\n",
|
| 12 |
+
"pre-caches all 7 specialist votes, runs **GRPO** on the moderator LoRA via\n",
|
| 13 |
+
"TRL, and saves loss / reward plots + the trained adapter.\n",
|
| 14 |
+
"\n",
|
| 15 |
+
"Designed for a free **Colab T4** (16 GB VRAM) — no separate vLLM server\n",
|
| 16 |
+
"needed. If you have an L4/A100, drop `load_in_4bit=False` for full bf16.\n",
|
| 17 |
+
"\n",
|
| 18 |
+
"Outputs live under `training/runs/<timestamp>/`:\n",
|
| 19 |
+
"- `moderator-lora/` — trained PEFT adapter\n",
|
| 20 |
+
"- `loss.png`, `reward.png` — training curves\n",
|
| 21 |
+
"- `eval_pre.json`, `eval_post.json` — pre/post backtest reports\n",
|
| 22 |
+
"\n",
|
| 23 |
+
"> Tip: `Runtime → Change runtime type → T4 GPU` before running."
|
| 24 |
+
]
|
| 25 |
},
|
| 26 |
{
|
| 27 |
"cell_type": "code",
|
| 28 |
+
"execution_count": 7,
|
| 29 |
"id": "install",
|
| 30 |
"metadata": {},
|
| 31 |
"outputs": [],
|
| 32 |
+
"source": [
|
| 33 |
+
"# Install — keep Colab's pre-baked pandas/numpy (TF + cudf depend on them);\n",
|
| 34 |
+
"# only upgrade what we strictly need.\n",
|
| 35 |
+
"#\n",
|
| 36 |
+
"# Big upgrades (transformers/trl/peft) are intentional — Colab ships old.\n",
|
| 37 |
+
"# Everything else uses --upgrade-strategy only-if-needed so we don't\n",
|
| 38 |
+
"# break google-colab/tensorflow/gradio/etc.\n",
|
| 39 |
+
"!pip install -q -U 'transformers>=4.55' 'trl>=0.11' 'peft>=0.13' 'accelerate>=1.0' 'bitsandbytes>=0.43' 'datasets>=3.0'\n",
|
| 40 |
+
"!pip install -q --upgrade-strategy=only-if-needed yfinance mplfinance pyarrow 'pydantic>=2' pydantic-settings 'openai>=1' tensorboard 'huggingface_hub>=1.10'\n",
|
| 41 |
+
"# Indicators are computed in-repo (app/data/indicators.py) — no pandas-ta."
|
| 42 |
+
]
|
| 43 |
},
|
| 44 |
{
|
| 45 |
"cell_type": "code",
|
| 46 |
+
"execution_count": 8,
|
| 47 |
"id": "clone",
|
| 48 |
"metadata": {},
|
| 49 |
+
"outputs": [
|
| 50 |
+
{
|
| 51 |
+
"name": "stdout",
|
| 52 |
+
"output_type": "stream",
|
| 53 |
+
"text": [
|
| 54 |
+
"Working dir: /content/stocker\n"
|
| 55 |
+
]
|
| 56 |
+
}
|
| 57 |
+
],
|
| 58 |
+
"source": [
|
| 59 |
+
"import os, sys, pathlib\n",
|
| 60 |
+
"\n",
|
| 61 |
+
"# 1) Already in a stocker repo? (running from VSCode on a local clone, or\n",
|
| 62 |
+
"# the repo was manually uploaded to Colab). Detect and skip the clone.\n",
|
| 63 |
+
"def _is_stocker_root(p: str) -> bool:\n",
|
| 64 |
+
" return os.path.isfile(os.path.join(p, \"app\", \"council\", \"specialists.py\"))\n",
|
| 65 |
+
"\n",
|
| 66 |
+
"CANDIDATES = [os.getcwd(), \"/content/stocker\", \"/workspace/stocker\"]\n",
|
| 67 |
+
"WORKDIR = next((c for c in CANDIDATES if _is_stocker_root(c)), None)\n",
|
| 68 |
+
"\n",
|
| 69 |
+
"if WORKDIR is None:\n",
|
| 70 |
+
" # 2) Not present — clone. EDIT THIS URL before running on a fresh Colab.\n",
|
| 71 |
+
" REPO_URL = \"https://github.com/<your-username>/stocker.git\"\n",
|
| 72 |
+
" WORKDIR = \"/content/stocker\"\n",
|
| 73 |
+
" assert \"<your-username>\" not in REPO_URL, (\n",
|
| 74 |
+
" \"Edit REPO_URL in this cell to your fork before running on a fresh runtime.\"\n",
|
| 75 |
+
" )\n",
|
| 76 |
+
" !git clone {REPO_URL} {WORKDIR}\n",
|
| 77 |
+
" assert _is_stocker_root(WORKDIR), f\"Clone failed — {WORKDIR}/app/council/ missing.\"\n",
|
| 78 |
+
"\n",
|
| 79 |
+
"os.chdir(WORKDIR)\n",
|
| 80 |
+
"sys.path.insert(0, WORKDIR)\n",
|
| 81 |
+
"print(\"Working dir:\", WORKDIR)"
|
| 82 |
+
]
|
| 83 |
},
|
| 84 |
{
|
| 85 |
"cell_type": "code",
|
|
|
|
| 87 |
"id": "auth",
|
| 88 |
"metadata": {},
|
| 89 |
"outputs": [],
|
| 90 |
+
"source": [
|
| 91 |
+
"# Gemma 4 is Apache-2.0 (not gated) — token just lifts download rate limits.\n",
|
| 92 |
+
"from huggingface_hub import login\n",
|
| 93 |
+
"import getpass\n",
|
| 94 |
+
"login(token=getpass.getpass(\"HF token (read scope is enough): \"))"
|
| 95 |
+
]
|
| 96 |
},
|
| 97 |
{
|
| 98 |
"cell_type": "code",
|
| 99 |
"execution_count": null,
|
| 100 |
"id": "build-data",
|
| 101 |
"metadata": {},
|
| 102 |
+
"outputs": [
|
| 103 |
+
{
|
| 104 |
+
"name": "stdout",
|
| 105 |
+
"output_type": "stream",
|
| 106 |
+
"text": [
|
| 107 |
+
"[1/6] Fetching OHLCV ...\n",
|
| 108 |
+
" prices: 536 rows\n",
|
| 109 |
+
"[2/6] Computing indicators ...\n",
|
| 110 |
+
" indicators: 536 rows\n",
|
| 111 |
+
"[3/6] Fetching peers + commodity ...\n",
|
| 112 |
+
" peers: 374 rows\n",
|
| 113 |
+
"[4/6] Loading curated news/forums/macro ...\n",
|
| 114 |
+
" news: 25 headlines, forums: 17 posts, macro: 12 events\n",
|
| 115 |
+
"[5/6] Rendering candlestick charts ...\n",
|
| 116 |
+
" charts: 126 PNGs in data/charts\n",
|
| 117 |
+
"[6/6] Done.\n",
|
| 118 |
+
"task_easy ticker=AAPL steps= 43 chart_ok=yes\n",
|
| 119 |
+
"task_medium ticker=INTC steps= 41 chart_ok=yes\n",
|
| 120 |
+
"task_hard ticker=META steps= 42 chart_ok=yes\n",
|
| 121 |
+
"\n",
|
| 122 |
+
"All 3 tasks OK.\n"
|
| 123 |
+
]
|
| 124 |
+
}
|
| 125 |
+
],
|
| 126 |
+
"source": [
|
| 127 |
+
"# Build the bundled dataset (yfinance + indicators + chart PNGs).\n",
|
| 128 |
+
"# Idempotent — skip if already done.\n",
|
| 129 |
+
"!python scripts/build_dataset.py\n",
|
| 130 |
+
"!python scripts/validate_tasks.py"
|
| 131 |
+
]
|
| 132 |
},
|
| 133 |
{
|
| 134 |
"cell_type": "code",
|
| 135 |
+
"execution_count": null,
|
| 136 |
"id": "f55a1108",
|
|
|
|
| 137 |
"metadata": {},
|
| 138 |
+
"outputs": [
|
| 139 |
+
{
|
| 140 |
+
"name": "stderr",
|
| 141 |
+
"output_type": "stream",
|
| 142 |
+
"text": [
|
| 143 |
+
"/usr/local/lib/python3.12/dist-packages/huggingface_hub/utils/_auth.py:103: UserWarning: \n",
|
| 144 |
+
"Error while fetching `HF_TOKEN` secret value from your vault: 'Requesting secret HF_TOKEN timed out. Secrets can only be fetched when running from the Colab UI.'.\n",
|
| 145 |
+
"You are not authenticated with the Hugging Face Hub in this notebook.\n",
|
| 146 |
+
"If the error persists, please let us know by opening an issue on GitHub (https://github.com/huggingface/huggingface_hub/issues/new).\n",
|
| 147 |
+
" warnings.warn(\n"
|
| 148 |
+
]
|
| 149 |
+
},
|
| 150 |
+
{
|
| 151 |
+
"data": {
|
| 152 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 153 |
+
"model_id": "09427ffe6e7b4be094cef43d13c33fe6",
|
| 154 |
+
"version_major": 2,
|
| 155 |
+
"version_minor": 0
|
| 156 |
+
},
|
| 157 |
+
"text/plain": [
|
| 158 |
+
"processor_config.json: 0.00B [00:00, ?B/s]"
|
| 159 |
+
]
|
| 160 |
+
},
|
| 161 |
+
"metadata": {},
|
| 162 |
+
"output_type": "display_data"
|
| 163 |
+
},
|
| 164 |
+
{
|
| 165 |
+
"data": {
|
| 166 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 167 |
+
"model_id": "8874cb7e313545a2b79bad9317858169",
|
| 168 |
+
"version_major": 2,
|
| 169 |
+
"version_minor": 0
|
| 170 |
+
},
|
| 171 |
+
"text/plain": [
|
| 172 |
+
"chat_template.jinja: 0.00B [00:00, ?B/s]"
|
| 173 |
+
]
|
| 174 |
+
},
|
| 175 |
+
"metadata": {},
|
| 176 |
+
"output_type": "display_data"
|
| 177 |
+
},
|
| 178 |
+
{
|
| 179 |
+
"data": {
|
| 180 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 181 |
+
"model_id": "37bb28cb29c64f97b2699f780174d9ee",
|
| 182 |
+
"version_major": 2,
|
| 183 |
+
"version_minor": 0
|
| 184 |
+
},
|
| 185 |
+
"text/plain": [
|
| 186 |
+
"config.json: 0.00B [00:00, ?B/s]"
|
| 187 |
+
]
|
| 188 |
+
},
|
| 189 |
+
"metadata": {},
|
| 190 |
+
"output_type": "display_data"
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
"data": {
|
| 194 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 195 |
+
"model_id": "459c2365fa5e4cf1ab6897f7a5e9816f",
|
| 196 |
+
"version_major": 2,
|
| 197 |
+
"version_minor": 0
|
| 198 |
+
},
|
| 199 |
+
"text/plain": [
|
| 200 |
+
"tokenizer_config.json: 0.00B [00:00, ?B/s]"
|
| 201 |
+
]
|
| 202 |
+
},
|
| 203 |
+
"metadata": {},
|
| 204 |
+
"output_type": "display_data"
|
| 205 |
+
},
|
| 206 |
+
{
|
| 207 |
+
"data": {
|
| 208 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 209 |
+
"model_id": "0f6eab584b334df89d7aa973094c3e58",
|
| 210 |
+
"version_major": 2,
|
| 211 |
+
"version_minor": 0
|
| 212 |
+
},
|
| 213 |
+
"text/plain": [
|
| 214 |
+
"tokenizer.json: 0%| | 0.00/32.2M [00:00<?, ?B/s]"
|
| 215 |
+
]
|
| 216 |
+
},
|
| 217 |
+
"metadata": {},
|
| 218 |
+
"output_type": "display_data"
|
| 219 |
+
},
|
| 220 |
+
{
|
| 221 |
+
"data": {
|
| 222 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 223 |
+
"model_id": "7c1bbf3af57b4782b0ea600e652b680a",
|
| 224 |
+
"version_major": 2,
|
| 225 |
+
"version_minor": 0
|
| 226 |
+
},
|
| 227 |
+
"text/plain": [
|
| 228 |
+
"model.safetensors: 0%| | 0.00/16.0G [00:00<?, ?B/s]"
|
| 229 |
+
]
|
| 230 |
+
},
|
| 231 |
+
"metadata": {},
|
| 232 |
+
"output_type": "display_data"
|
| 233 |
+
},
|
| 234 |
+
{
|
| 235 |
+
"data": {
|
| 236 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 237 |
+
"model_id": "f5cfa2f28b1844cd9808cf55cfdfc9db",
|
| 238 |
+
"version_major": 2,
|
| 239 |
+
"version_minor": 0
|
| 240 |
+
},
|
| 241 |
+
"text/plain": [
|
| 242 |
+
"Loading weights: 0%| | 0/2076 [00:00<?, ?it/s]"
|
| 243 |
+
]
|
| 244 |
+
},
|
| 245 |
+
"metadata": {},
|
| 246 |
+
"output_type": "display_data"
|
| 247 |
+
},
|
| 248 |
+
{
|
| 249 |
+
"data": {
|
| 250 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 251 |
+
"model_id": "925f4bb541bf49a5b42067ba6e4c10a9",
|
| 252 |
+
"version_major": 2,
|
| 253 |
+
"version_minor": 0
|
| 254 |
+
},
|
| 255 |
+
"text/plain": [
|
| 256 |
+
"generation_config.json: 0%| | 0.00/208 [00:00<?, ?B/s]"
|
| 257 |
+
]
|
| 258 |
+
},
|
| 259 |
+
"metadata": {},
|
| 260 |
+
"output_type": "display_data"
|
| 261 |
+
},
|
| 262 |
+
{
|
| 263 |
+
"name": "stdout",
|
| 264 |
+
"output_type": "stream",
|
| 265 |
+
"text": [
|
| 266 |
+
"Model loaded on: cuda:0\n"
|
| 267 |
+
]
|
| 268 |
+
}
|
| 269 |
+
],
|
| 270 |
+
"source": [
|
| 271 |
+
"from app.council.llm import TransformersLLMClient\n",
|
| 272 |
+
"\n",
|
| 273 |
+
"# 4-bit BnB by default; drop load_in_4bit=False on L4/A100.\n",
|
| 274 |
+
"client = TransformersLLMClient.from_pretrained(\n",
|
| 275 |
+
" \"google/gemma-4-E4B-it\",\n",
|
| 276 |
+
" load_in_4bit=True,\n",
|
| 277 |
+
")\n",
|
| 278 |
+
"print(\"Model loaded on:\", client.model.device)"
|
| 279 |
+
]
|
| 280 |
},
|
| 281 |
{
|
| 282 |
"cell_type": "code",
|
| 283 |
+
"execution_count": null,
|
| 284 |
"id": "a3a9bd4c",
|
|
|
|
| 285 |
"metadata": {},
|
| 286 |
+
"outputs": [
|
| 287 |
+
{
|
| 288 |
+
"ename": "TypeError",
|
| 289 |
+
"evalue": "string indices must be integers, not 'str'",
|
| 290 |
+
"output_type": "error",
|
| 291 |
+
"traceback": [
|
| 292 |
+
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
|
| 293 |
+
"\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)",
|
| 294 |
+
"\u001b[0;32m/tmp/ipykernel_1481/2753751009.py\u001b[0m in \u001b[0;36m<cell line: 0>\u001b[0;34m()\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 13\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0msp\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mcouncil\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mspecialists\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 14\u001b[0;31m \u001b[0mcouncil\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_cached_vote\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msp\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mobs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m# writes .cache/council/<role>/...\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 15\u001b[0m \u001b[0mtotal\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 16\u001b[0m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0menv\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mstep\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m{\u001b[0m\u001b[0;34m\"side\"\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m\"hold\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"quantity\"\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
|
| 295 |
+
"\u001b[0;32m/content/stocker/app/council/runner.py\u001b[0m in \u001b[0;36m_cached_vote\u001b[0;34m(self, sp, obs)\u001b[0m\n\u001b[1;32m 73\u001b[0m \u001b[0;32mpass\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 74\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 75\u001b[0;31m \u001b[0mvote\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvote\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mobs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 76\u001b[0m \u001b[0mkey\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mparent\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmkdir\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mparents\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mexist_ok\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 77\u001b[0m \u001b[0mkey\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwrite_text\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvote\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmodel_dump_json\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
|
| 296 |
+
"\u001b[0;32m/content/stocker/app/council/specialists.py\u001b[0m in \u001b[0;36mvote\u001b[0;34m(self, obs)\u001b[0m\n\u001b[1;32m 30\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mvote\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mobs\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mMarketObservation\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0mSpecialistVote\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 31\u001b[0m \u001b[0mmessages\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mprepare_messages\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mobs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 32\u001b[0;31m \u001b[0mtext\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mclient\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcomplete\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmessages\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmax_tokens\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m256\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtemperature\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m0.2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 33\u001b[0m \u001b[0mdata\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mparse_json_object\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtext\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 34\u001b[0m return SpecialistVote(\n",
|
| 297 |
+
"\u001b[0;32m/content/stocker/app/council/llm.py\u001b[0m in \u001b[0;36mcomplete\u001b[0;34m(self, messages, max_tokens, temperature, extra_body)\u001b[0m\n\u001b[1;32m 209\u001b[0m \u001b[0;31m# the processor expects.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 210\u001b[0m \u001b[0mprepped\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_prep_messages\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmessages\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 211\u001b[0;31m inputs = self.processor.apply_chat_template(\n\u001b[0m\u001b[1;32m 212\u001b[0m \u001b[0mprepped\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 213\u001b[0m \u001b[0madd_generation_prompt\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
|
| 298 |
+
"\u001b[0;32m/usr/local/lib/python3.12/dist-packages/transformers/processing_utils.py\u001b[0m in \u001b[0;36mapply_chat_template\u001b[0;34m(self, conversation, chat_template, tools, documents, add_generation_prompt, continue_final_message, return_assistant_tokens_mask, tokenize, return_tensors, return_dict, load_audio_from_video, processor_kwargs, **kwargs)\u001b[0m\n\u001b[1;32m 1815\u001b[0m \u001b[0mcontent\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmessage\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"content\"\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1816\u001b[0m visuals = [\n\u001b[0;32m-> 1817\u001b[0;31m \u001b[0mcontent_block\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mcontent_block\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mcontent\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mcontent_block\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"type\"\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32min\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m\"image\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"video\"\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1818\u001b[0m ]\n\u001b[1;32m 1819\u001b[0m audio_fnames = [\n",
|
| 299 |
+
"\u001b[0;31mTypeError\u001b[0m: string indices must be integers, not 'str'"
|
| 300 |
+
]
|
| 301 |
+
}
|
| 302 |
+
],
|
| 303 |
+
"source": [
|
| 304 |
+
"# Pre-cache the 7 specialists' votes for every (task, step). Specialists\n",
|
| 305 |
+
"# are FROZEN, so this runs once and is reused across every GRPO step.\n",
|
| 306 |
+
"from app.council.runner import Council\n",
|
| 307 |
+
"from app.core.environment import StockerEnv\n",
|
| 308 |
+
"from app.core.tasks import list_task_ids\n",
|
| 309 |
+
"\n",
|
| 310 |
+
"council = Council(client=client, use_cache=True)\n",
|
| 311 |
+
"total = 0\n",
|
| 312 |
+
"for task_id in list_task_ids():\n",
|
| 313 |
+
" env = StockerEnv(task_id=task_id)\n",
|
| 314 |
+
" obs = env.reset().observation\n",
|
| 315 |
+
" while True:\n",
|
| 316 |
+
" for sp in council.specialists:\n",
|
| 317 |
+
" council._cached_vote(sp, obs) # writes .cache/council/<role>/...\n",
|
| 318 |
+
" total += 1\n",
|
| 319 |
+
" result = env.step({\"side\": \"hold\", \"quantity\": 0})\n",
|
| 320 |
+
" if result.done:\n",
|
| 321 |
+
" break\n",
|
| 322 |
+
" obs = result.observation\n",
|
| 323 |
+
"print(f\"cached {total} specialist votes across {len(list_task_ids())} tasks\")"
|
| 324 |
+
]
|
| 325 |
},
|
| 326 |
{
|
| 327 |
"cell_type": "code",
|
| 328 |
+
"execution_count": null,
|
| 329 |
"id": "6af95f46",
|
|
|
|
| 330 |
"metadata": {},
|
| 331 |
+
"outputs": [],
|
| 332 |
+
"source": [
|
| 333 |
+
"# Pre-training baseline rollout (specialists from cache, moderator = base Gemma)\n",
|
| 334 |
+
"!python -m training.eval_rollout --out training/runs/eval_pre\n",
|
| 335 |
+
"!cat training/runs/eval_pre/summary.csv"
|
| 336 |
+
]
|
| 337 |
},
|
| 338 |
{
|
| 339 |
"cell_type": "code",
|
|
|
|
| 341 |
"id": "train",
|
| 342 |
"metadata": {},
|
| 343 |
"outputs": [],
|
| 344 |
+
"source": [
|
| 345 |
+
"# GRPO on the moderator LoRA. Specialist votes are read from cache —\n",
|
| 346 |
+
"# only the moderator is re-rolled per training step, so this is fast.\n",
|
| 347 |
+
"#\n",
|
| 348 |
+
"# Knobs: --num-generations is K candidates per prompt (GRPO group size).\n",
|
| 349 |
+
"# On a free T4 keep batch_size=1, grad_accum=8.\n",
|
| 350 |
+
"!python -m training.train_grpo \\\n",
|
| 351 |
+
" --epochs 2 \\\n",
|
| 352 |
+
" --num-generations 8 \\\n",
|
| 353 |
+
" --batch-size 1 \\\n",
|
| 354 |
+
" --grad-accum 8 \\\n",
|
| 355 |
+
" --lora-rank 16 \\\n",
|
| 356 |
+
" --lr 5e-6"
|
| 357 |
+
]
|
| 358 |
},
|
| 359 |
{
|
| 360 |
"cell_type": "code",
|
|
|
|
| 362 |
"id": "eval",
|
| 363 |
"metadata": {},
|
| 364 |
"outputs": [],
|
| 365 |
+
"source": [
|
| 366 |
+
"# Post-training eval — load the trained adapter into the same client.\n",
|
| 367 |
+
"import glob, os\n",
|
| 368 |
+
"RUN_DIR = sorted(glob.glob(\"training/runs/grpo_*\"))[-1]\n",
|
| 369 |
+
"LORA_DIR = os.path.join(RUN_DIR, \"moderator-lora\")\n",
|
| 370 |
+
"print(\"Using LoRA:\", LORA_DIR)\n",
|
| 371 |
+
"\n",
|
| 372 |
+
"# Attach adapter and re-run eval. The adapter name \"moderator\" matches what\n",
|
| 373 |
+
"# Moderator.decide() requests via extra_body={'lora_request': {'name': ...}}\n",
|
| 374 |
+
"from peft import PeftModel\n",
|
| 375 |
+
"client.model = PeftModel.from_pretrained(client.model, LORA_DIR, adapter_name=\"moderator\")\n",
|
| 376 |
+
"client.moderator_lora = \"moderator\"\n",
|
| 377 |
+
"\n",
|
| 378 |
+
"!python -m training.eval_rollout --moderator-lora moderator --out training/runs/eval_post\n",
|
| 379 |
+
"!cat training/runs/eval_post/summary.csv"
|
| 380 |
+
]
|
| 381 |
},
|
| 382 |
{
|
| 383 |
"cell_type": "code",
|
| 384 |
+
"execution_count": null,
|
| 385 |
"id": "b1d888ed",
|
|
|
|
| 386 |
"metadata": {},
|
| 387 |
+
"outputs": [],
|
| 388 |
+
"source": [
|
| 389 |
+
"# Show training + eval curves inline\n",
|
| 390 |
+
"from IPython.display import Image, display\n",
|
| 391 |
+
"import os\n",
|
| 392 |
+
"for p in [f\"{RUN_DIR}/loss.png\", f\"{RUN_DIR}/reward.png\",\n",
|
| 393 |
+
" \"training/runs/eval_pre/reward_curve.png\",\n",
|
| 394 |
+
" \"training/runs/eval_post/reward_curve.png\",\n",
|
| 395 |
+
" \"training/runs/eval_pre/portfolio_curve.png\",\n",
|
| 396 |
+
" \"training/runs/eval_post/portfolio_curve.png\"]:\n",
|
| 397 |
+
" if os.path.exists(p):\n",
|
| 398 |
+
" print(p)\n",
|
| 399 |
+
" display(Image(p))"
|
| 400 |
+
]
|
| 401 |
}
|
| 402 |
],
|
| 403 |
"metadata": {
|
| 404 |
"kernelspec": {
|
| 405 |
+
"display_name": "Python 3 (ipykernel)",
|
| 406 |
"language": "python",
|
| 407 |
"name": "python3"
|
| 408 |
},
|
| 409 |
"language_info": {
|
| 410 |
+
"codemirror_mode": {
|
| 411 |
+
"name": "ipython",
|
| 412 |
+
"version": 3
|
| 413 |
+
},
|
| 414 |
+
"file_extension": ".py",
|
| 415 |
+
"mimetype": "text/x-python",
|
| 416 |
+
"name": "python",
|
| 417 |
+
"nbconvert_exporter": "python",
|
| 418 |
+
"pygments_lexer": "ipython3",
|
| 419 |
+
"version": "3.12.13"
|
| 420 |
}
|
| 421 |
},
|
| 422 |
"nbformat": 4,
|
| 423 |
"nbformat_minor": 5
|
| 424 |
+
}
|