11import dspy
2- import polars as pl
3- from typing import Optional , Dict , Any , List , Tuple
4- import json
2+ from typing import Optional , Dict , Any , Tuple
53from datetime import datetime
64import dagster as dg
75from pydantic import Field
1210
1311class AssetAllocationSignature (dspy .Signature ):
1412 """Generate specific asset allocation recommendations based on economic cycle analysis and market trends."""
15-
13+
1614 economic_cycle_analysis : str = dspy .InputField (
1715 desc = "Economic cycle analysis including current cycle position and key indicators"
1816 )
19-
17+
2018 market_trend_analysis : str = dspy .InputField (
2119 desc = "Market trend analysis including sector performance and momentum indicators"
2220 )
23-
21+
2422 current_portfolio_context : str = dspy .InputField (
2523 desc = "Current portfolio context and constraints (optional)"
2624 )
27-
25+
2826 allocation_recommendations : str = dspy .OutputField (
2927 desc = """Detailed asset allocation recommendations including:
3028 1. Portfolio Allocation by Asset Class:
@@ -60,32 +58,37 @@ class AssetAllocationModule(dspy.Module):
6058 def __init__ (self ):
6159 super ().__init__ ()
6260 self .analyze_allocation = dspy .ChainOfThought (AssetAllocationSignature )
63-
64- def forward (self , economic_cycle_analysis : str , market_trend_analysis : str , current_portfolio_context : str = "" ):
61+
62+ def forward (
63+ self ,
64+ economic_cycle_analysis : str ,
65+ market_trend_analysis : str ,
66+ current_portfolio_context : str = "" ,
67+ ):
6568 return self .analyze_allocation (
6669 economic_cycle_analysis = economic_cycle_analysis ,
6770 market_trend_analysis = market_trend_analysis ,
68- current_portfolio_context = current_portfolio_context
71+ current_portfolio_context = current_portfolio_context ,
6972 )
7073
7174
7275class AssetAllocationAnalyzer (dg .ConfigurableResource ):
7376 """Asset allocation analyzer that provides specific investment recommendations."""
74-
77+
7578 model_name : str = Field (
7679 default = "gpt-4-turbo-preview" , description = "LLM model to use for analysis"
7780 )
7881 openai_api_key : str = Field (description = "OpenAI API key for DSPy" )
79-
82+
8083 def setup_for_execution (self , context ) -> None :
8184 """Initialize DSPy when the resource is used."""
8285 # Initialize DSPy
8386 lm = dspy .LM (model = self .model_name , api_key = self .openai_api_key )
8487 dspy .settings .configure (lm = lm )
85-
88+
8689 # Initialize analyzer
8790 self ._allocation_analyzer = AssetAllocationModule ()
88-
91+
8992 @property
9093 def allocation_analyzer (self ):
9194 """Get asset allocation analyzer."""
@@ -102,46 +105,48 @@ def get_latest_analysis(self, md_resource: MotherDuckResource) -> Tuple[str, str
102105 )
103106 ORDER BY analysis_type
104107 """
105-
108+
106109 df = md_resource .execute_query (query , read_only = True )
107-
110+
108111 cycle_analysis = ""
109112 trend_analysis = ""
110-
113+
111114 for row in df .iter_rows (named = True ):
112115 if row ["analysis_type" ] == "economic_cycle" :
113116 cycle_analysis = row ["analysis_content" ]
114117 elif row ["analysis_type" ] == "market_trends" :
115118 trend_analysis = row ["analysis_content" ]
116-
119+
117120 return cycle_analysis , trend_analysis
118121
119122 def generate_allocation_recommendations (
120123 self ,
121124 md_resource : MotherDuckResource ,
122125 portfolio_context : str = "" ,
123- context : Optional [dg .AssetExecutionContext ] = None
126+ context : Optional [dg .AssetExecutionContext ] = None ,
124127 ) -> Dict [str , Any ]:
125128 """Generate asset allocation recommendations based on latest analysis."""
126129 if context :
127130 context .log .info ("Retrieving latest economic and market analysis..." )
128-
131+
129132 # Get latest analysis
130133 cycle_analysis , trend_analysis = self .get_latest_analysis (md_resource )
131-
134+
132135 if not cycle_analysis or not trend_analysis :
133- raise ValueError ("No recent economic cycle or market trend analysis found. Please run economic_cycle_analysis first." )
134-
136+ raise ValueError (
137+ "No recent economic cycle or market trend analysis found. Please run economic_cycle_analysis first."
138+ )
139+
135140 if context :
136141 context .log .info ("Generating asset allocation recommendations..." )
137-
142+
138143 # Generate recommendations
139144 allocation_result = self .allocation_analyzer (
140145 economic_cycle_analysis = cycle_analysis ,
141146 market_trend_analysis = trend_analysis ,
142- current_portfolio_context = portfolio_context
147+ current_portfolio_context = portfolio_context ,
143148 )
144-
149+
145150 # Format results
146151 analysis_timestamp = datetime .now ()
147152 result = {
@@ -151,9 +156,11 @@ def generate_allocation_recommendations(
151156 "model_name" : self .model_name ,
152157 "allocation_recommendations" : allocation_result .allocation_recommendations ,
153158 "portfolio_context" : portfolio_context ,
154- "source_analysis_timestamp" : self ._get_latest_analysis_timestamp (md_resource )
159+ "source_analysis_timestamp" : self ._get_latest_analysis_timestamp (
160+ md_resource
161+ ),
155162 }
156-
163+
157164 return result
158165
159166 def _get_latest_analysis_timestamp (self , md_resource : MotherDuckResource ) -> str :
@@ -162,14 +169,14 @@ def _get_latest_analysis_timestamp(self, md_resource: MotherDuckResource) -> str
162169 SELECT MAX(analysis_timestamp) as latest_timestamp
163170 FROM economic_cycle_analysis
164171 """
165-
172+
166173 df = md_resource .execute_query (query , read_only = True )
167174 return df [0 , "latest_timestamp" ] if not df .is_empty () else ""
168175
169176 def format_allocation_as_json (
170177 self ,
171178 allocation_result : Dict [str , Any ],
172- metadata : Optional [Dict [str , Any ]] = None
179+ metadata : Optional [Dict [str , Any ]] = None ,
173180 ) -> Dict [str , Any ]:
174181 """Format allocation recommendations as JSON record."""
175182 json_result = {
@@ -180,13 +187,13 @@ def format_allocation_as_json(
180187 "analysis_time" : allocation_result ["analysis_time" ],
181188 "model_name" : allocation_result ["model_name" ],
182189 "portfolio_context" : allocation_result ["portfolio_context" ],
183- "source_analysis_timestamp" : allocation_result ["source_analysis_timestamp" ]
190+ "source_analysis_timestamp" : allocation_result ["source_analysis_timestamp" ],
184191 }
185-
192+
186193 # Add metadata if provided
187194 if metadata :
188195 json_result .update (metadata )
189-
196+
190197 return json_result
191198
192199 def write_allocation_to_table (
@@ -195,24 +202,24 @@ def write_allocation_to_table(
195202 allocation_result : Dict [str , Any ],
196203 output_table : str = "asset_allocation_recommendations" ,
197204 if_exists : str = "append" ,
198- context : Optional [dg .AssetExecutionContext ] = None
205+ context : Optional [dg .AssetExecutionContext ] = None ,
199206 ) -> None :
200207 """Write allocation recommendations to database."""
201208 # Format results as JSON
202209 json_result = self .format_allocation_as_json (
203210 allocation_result ,
204211 metadata = {
205212 "dagster_run_id" : context .run_id if context else None ,
206- "dagster_asset_key" : str (context .asset_key ) if context else None
207- }
213+ "dagster_asset_key" : str (context .asset_key ) if context else None ,
214+ },
208215 )
209-
216+
210217 # Write to database
211218 md_resource .write_results_to_table (
212219 [json_result ],
213220 output_table = output_table ,
214221 if_exists = if_exists ,
215- context = context
222+ context = context ,
216223 )
217224
218225
@@ -229,39 +236,39 @@ def asset_allocation_recommendations(
229236) -> Dict [str , Any ]:
230237 """
231238 Asset that generates specific asset allocation recommendations based on economic analysis.
232-
239+
233240 Returns:
234241 Dictionary with allocation recommendations and metadata
235242 """
236243 context .log .info ("Starting asset allocation analysis..." )
237-
244+
238245 # Generate recommendations
239246 allocation_result = allocation_analyzer .generate_allocation_recommendations (
240247 md_resource = md ,
241248 portfolio_context = "General portfolio - no specific constraints" ,
242- context = context
249+ context = context ,
243250 )
244-
251+
245252 # Write results to database
246253 context .log .info ("Writing allocation recommendations to database..." )
247254 allocation_analyzer .write_allocation_to_table (
248255 md_resource = md ,
249256 allocation_result = allocation_result ,
250257 output_table = "asset_allocation_recommendations" ,
251258 if_exists = "append" ,
252- context = context
259+ context = context ,
253260 )
254-
261+
255262 # Return metadata
256263 result_metadata = {
257264 "analysis_completed" : True ,
258265 "analysis_timestamp" : allocation_result ["analysis_timestamp" ],
259266 "model_name" : allocation_result ["model_name" ],
260267 "output_table" : "asset_allocation_recommendations" ,
261268 "records_written" : 1 ,
262- "source_analysis_timestamp" : allocation_result ["source_analysis_timestamp" ]
269+ "source_analysis_timestamp" : allocation_result ["source_analysis_timestamp" ],
263270 }
264-
271+
265272 context .log .info (f"Asset allocation analysis complete: { result_metadata } " )
266273 return result_metadata
267274
@@ -279,14 +286,14 @@ def custom_asset_allocation(
279286) -> Dict [str , Any ]:
280287 """
281288 Asset that generates custom asset allocation recommendations with specific portfolio context.
282-
289+
283290 This asset can be customized with different portfolio contexts by modifying the portfolio_context parameter.
284-
291+
285292 Returns:
286293 Dictionary with custom allocation recommendations and metadata
287294 """
288295 context .log .info ("Starting custom asset allocation analysis..." )
289-
296+
290297 # Define custom portfolio context
291298 custom_context = """
292299 Portfolio Context:
@@ -298,24 +305,22 @@ def custom_asset_allocation(
298305 - Liquidity needs: Moderate (some funds may be needed within 2-3 years)
299306 - Tax considerations: Taxable account, prefer tax-efficient strategies
300307 """
301-
308+
302309 # Generate custom recommendations
303310 allocation_result = allocation_analyzer .generate_allocation_recommendations (
304- md_resource = md ,
305- portfolio_context = custom_context ,
306- context = context
311+ md_resource = md , portfolio_context = custom_context , context = context
307312 )
308-
313+
309314 # Write results to database
310315 context .log .info ("Writing custom allocation recommendations to database..." )
311316 allocation_analyzer .write_allocation_to_table (
312317 md_resource = md ,
313318 allocation_result = allocation_result ,
314319 output_table = "custom_asset_allocation_recommendations" ,
315320 if_exists = "append" ,
316- context = context
321+ context = context ,
317322 )
318-
323+
319324 # Return metadata
320325 result_metadata = {
321326 "analysis_completed" : True ,
@@ -324,8 +329,8 @@ def custom_asset_allocation(
324329 "output_table" : "custom_asset_allocation_recommendations" ,
325330 "records_written" : 1 ,
326331 "portfolio_context" : "Conservative retirement-focused" ,
327- "source_analysis_timestamp" : allocation_result ["source_analysis_timestamp" ]
332+ "source_analysis_timestamp" : allocation_result ["source_analysis_timestamp" ],
328333 }
329-
334+
330335 context .log .info (f"Custom asset allocation analysis complete: { result_metadata } " )
331336 return result_metadata
0 commit comments