Skip to content

Commit 2ba5fa2

Browse files
authored
Merge pull request #2 from Breta01/agentic
Agentic
2 parents cbb5c79 + a64c1c1 commit 2ba5fa2

38 files changed

Lines changed: 1298 additions & 32 deletions

.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,12 @@ OPENAI_API_KEY=
77

88
# Database URL
99
DATABASE_URL=
10+
11+
# Competition ID (update accordingly)
12+
COMPETITION_ID=IQC2025S2
13+
14+
# Langsmith Tracing
15+
LANGSMITH_TRACING=true
16+
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
17+
LANGSMITH_API_KEY=
18+
LANGSMITH_PROJECT=

brain/agent_config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from langchain_core.runnables import RunnableConfig
22

3+
from brain.alpha_class import Alpha
4+
35
DEFAULT_CONFIG = {
46
"region": "USA",
57
"universe": "TOP3000",
@@ -17,3 +19,15 @@ def get_universe_config(config: RunnableConfig) -> dict:
1719
**DEFAULT_CONFIG,
1820
**{k: conf[k] for k in DEFAULT_CONFIG.keys() if k in conf},
1921
}
22+
23+
24+
def get_config(alpha: Alpha) -> dict:
25+
"""Get the configuration for the alpha."""
26+
return {
27+
"region": alpha.region,
28+
"universe": alpha.universe,
29+
"neutralization": alpha.neutralization,
30+
"truncation": alpha.truncation,
31+
"decay": alpha.decay,
32+
"delay": alpha.delay,
33+
}

brain/agentic.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
from langgraph.graph import END, START, StateGraph
2+
3+
from brain.agents import (
4+
invoke_executor,
5+
invoke_fine_tuner,
6+
invoke_planner,
7+
invoke_seeder,
8+
invoke_tester,
9+
)
10+
from brain.alpha_storage import Storage
11+
from brain.api import BrainAPI
12+
from brain.graph_state import GraphState
13+
from brain.score import get_score
14+
15+
MAX_EXPLORATION_COUNT = 3
16+
17+
18+
def plateau_condition(state: GraphState) -> bool:
19+
"""Check if the exploration has plateaued."""
20+
storage = state["storage"]
21+
best_alpha = storage.best_alpha
22+
old_best_alpha = state.get("old_best_alpha")
23+
24+
if best_alpha is None or old_best_alpha is None:
25+
return False
26+
27+
# Check if the score has not improved significantly
28+
score_diff = (
29+
best_alpha.fitness - old_best_alpha.fitness + best_alpha.sharpe - old_best_alpha.sharpe
30+
)
31+
return score_diff < 0.01
32+
33+
34+
def seed_finder_node(state: GraphState) -> GraphState:
35+
"""Find a seed alpha to start the exploration."""
36+
# Iterate databse till we find some decent alpha, or some other seed idea
37+
alpha_idea, config = invoke_seeder(state)
38+
print(f"Seed alpha: {alpha_idea}")
39+
return {
40+
"alpha_idea": alpha_idea,
41+
"default_config": config,
42+
"node": "plan",
43+
"state": "explore",
44+
"explore_count": 0,
45+
"static_finetune": True,
46+
"storage": Storage(score_func=get_score, max_size=50),
47+
}
48+
49+
50+
def planner_node(state: GraphState) -> GraphState:
51+
"""Plan the next steps based on the current alpha and state."""
52+
state["explore_count"] += 1
53+
state["old_best_alpha"] = state["storage"].best_alpha
54+
55+
plan = invoke_planner(state)
56+
if not plan:
57+
return {**state, "node": "seed"}
58+
59+
if state["storage"].best_alpha is None:
60+
plan.insert(0, "Execute initial alpha idea to obtain baseline")
61+
62+
plan.append(
63+
"Think about previous changes and how they affected the alpha."
64+
" Propose new alphas based on the most successful changes."
65+
)
66+
67+
return {**state, "node": "execute", "plan": plan}
68+
69+
70+
def executor_node(state: GraphState) -> GraphState:
71+
state = invoke_executor(state)
72+
# TODO: Pass some summary of results from the executor to planner
73+
return {**state, "node": "explore_test", "state": "explore"}
74+
75+
76+
def fine_tuner_node(state: GraphState) -> GraphState:
77+
invoke_fine_tuner(state)
78+
return {**state, "node": "submit_test", "state": "fine_tune", "static_finetune": False}
79+
80+
81+
def explore_test_node(state: GraphState) -> GraphState:
82+
"""Decide what happens next after exploring a new alpha idea."""
83+
best_alpha = state["storage"].best_alpha
84+
old_best_alpha = state.get("old_best_alpha")
85+
86+
if (
87+
best_alpha is not None
88+
and (old_best_alpha is None or old_best_alpha.alpha_id != best_alpha.alpha_id)
89+
and len(best_alpha.failing_tests) == 0
90+
):
91+
score = best_alpha.update_score()
92+
trade_count = best_alpha.long_count + best_alpha.short_count
93+
print("Best alpha score:", score, "Trade count:", trade_count)
94+
if score > -50 and trade_count > 400 and invoke_tester(state):
95+
return {
96+
**state,
97+
"node": "fine_tuner",
98+
"state": "fine_tune",
99+
"static_finetune": True,
100+
"explore_count": 0,
101+
}
102+
103+
# TODO: Test plateau condition, compare previous best, with current best alpha
104+
if state["explore_count"] < MAX_EXPLORATION_COUNT and plateau_condition(state):
105+
return {**state, "node": "plan", "state": "explore"}
106+
107+
return {**state, "node": "seed", "state": "explore"}
108+
109+
110+
def submit_test_node(state: GraphState) -> GraphState:
111+
"""Decide what happens next after fine-tuning a new alpha idea."""
112+
best_alpha = state["storage"].best_alpha
113+
old_best_alpha = state.get("old_best_alpha")
114+
115+
if (
116+
best_alpha is not None
117+
and (old_best_alpha is None or old_best_alpha.alpha_id != best_alpha.alpha_id)
118+
and len(best_alpha.failing_tests) == 0
119+
):
120+
score = best_alpha.update_score()
121+
trade_count = best_alpha.long_count + best_alpha.short_count
122+
print("Best alpha score:", score, "Trade count:", trade_count)
123+
if score > 200 and trade_count > 400 and invoke_tester(state):
124+
# TODO: Mark alpha as "submitted" or "ready for production"
125+
print("Submitting alpha! Score:", score)
126+
BrainAPI.submit_alpha(best_alpha.alpha_id)
127+
return {**state, "node": "seed", "state": "explore", "explore_count": 0}
128+
129+
# TODO: Test plateau condition, compare previous best, with current best alpha
130+
if state["explore_count"] < MAX_EXPLORATION_COUNT and plateau_condition(state):
131+
return {**state, "node": "plan", "state": "fine_tune"}
132+
133+
return {**state, "node": "seed", "state": "explore"}
134+
135+
136+
builder = StateGraph(GraphState)
137+
138+
builder.add_node("seed_finder", seed_finder_node)
139+
builder.add_node("planner", planner_node)
140+
builder.add_node("executor", executor_node)
141+
builder.add_node("fine_tuner", fine_tuner_node)
142+
builder.add_node("explore_test", explore_test_node)
143+
builder.add_node("submit_test", submit_test_node)
144+
# builder.add_node("stagnation_chk", stagnation_node)
145+
146+
147+
# Static flow
148+
builder.add_edge(START, "seed_finder")
149+
builder.add_edge("executor", "explore_test")
150+
builder.add_edge("fine_tuner", "submit_test")
151+
152+
# Conditional branching (no more …then= kwarg in 0.4.8)
153+
for node in ["seed_finder", "planner", "explore_test", "submit_test"]:
154+
builder.add_conditional_edges(
155+
node,
156+
lambda state: state["node"],
157+
path_map={
158+
"plan": "planner",
159+
"execute": "executor",
160+
"seed": "seed_finder",
161+
"fine_tuner": "fine_tuner",
162+
"explore_test": "explore_test",
163+
"submit_test": "submit_test",
164+
"stop": END,
165+
},
166+
)
167+
168+
graph = builder.compile() # returns a CompiledStateGraph

brain/agents/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
__all__ = [
2+
"invoke_executor",
3+
"invoke_fine_tuner",
4+
"invoke_planner",
5+
"invoke_tester",
6+
"invoke_seeder",
7+
]
8+
9+
from .executor import invoke as invoke_executor
10+
from .fine_tuner import invoke as invoke_fine_tuner
11+
from .planner import invoke as invoke_planner
12+
from .seeder import invoke as invoke_seeder
13+
from .tester import invoke as invoke_tester

brain/agents/alpha_tester.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from functools import partial
2+
3+
from brain.agents.executor import create_alpha_simulation
4+
from brain.alpha_class import Alpha
5+
from brain.genetic_algorithm import execute_alphas, genetic_algorithm
6+
from brain.graph_state import GraphState
7+
8+
param_options = {
9+
"universe": ["TOP3000", "TOP1000", "TOP500", "TOP200"],
10+
"neutralization": ["INDUSTRY", "SECTOR", "MARKET", "NONE", "SUBINDUSTRY"],
11+
"decay": [2, 4, 6, 8, 10, 12, 14, 16, 20],
12+
"truncation": [0.005, 0.01, 0.05, 0.1],
13+
# "pasteurization": ["ON", "OFF"],
14+
}
15+
16+
17+
def get_config(alpha: Alpha) -> dict:
18+
"""Get the configuration for the alpha."""
19+
return {
20+
"region": alpha.region,
21+
"universe": alpha.universe,
22+
"neutralization": alpha.neutralization,
23+
"truncation": alpha.truncation,
24+
"decay": alpha.decay,
25+
"delay": alpha.delay,
26+
}
27+
28+
29+
def invoke(state: GraphState) -> GraphState:
30+
"""Invoke fine-tuning agent."""
31+
storage = state.get("storage")
32+
# Rank, sign, change delay +-1 (check for 0), truncation +-0.01
33+
34+
for param, options in param_options.items():
35+
alphas = [storage.best_alpha.replace(**{param: option}) for option in options]
36+
list(execute_alphas(alphas, storage))
37+
38+
congig = get_config(storage.best_alpha)
39+
plan = state.get("plan", [])
40+
create_alpha = partial(create_alpha_simulation, plan=plan, config=congig)
41+
42+
storage.reset_counter()
43+
genetic_algorithm(storage, create_alpha, len(plan) + 10)

0 commit comments

Comments
 (0)