44
55import pandas as pd
66import yaml
7+ import yfinance as yf
78
89from ingest_prices import PriceIngestor
910from llm_trader import propose_trades_with_llm
1011
1112
13+ REQUIRED_PRICE_COLUMNS = ["date" , "open" , "high" , "low" , "close" , "volume" ]
14+
15+
1216def load_config (path = "config.yaml" ):
1317 with open (path , "r" ) as handle :
1418 return yaml .safe_load (handle )
1519
1620
21+ def _normalize_prices (symbol , prices_df ):
22+ if prices_df is None :
23+ return None
24+ df = prices_df .copy ()
25+ if df .empty :
26+ return None
27+
28+ if "date" not in df .columns :
29+ df = df .reset_index ()
30+
31+ normalized_columns = {}
32+ for col in df .columns :
33+ key = str (col ).strip ().lower ().replace (" " , "_" )
34+ normalized_columns [col ] = key
35+ df = df .rename (columns = normalized_columns )
36+
37+ if "datetime" in df .columns and "date" not in df .columns :
38+ df = df .rename (columns = {"datetime" : "date" })
39+ if "adj_close" in df .columns and "close" not in df .columns :
40+ df = df .rename (columns = {"adj_close" : "close" })
41+
42+ if "date" not in df .columns :
43+ return None
44+
45+ if "volume" not in df .columns :
46+ df ["volume" ] = 0
47+ for col in ["open" , "high" , "low" , "close" ]:
48+ if col not in df .columns :
49+ return None
50+
51+ df ["symbol" ] = str (symbol or "" ).strip ().upper ()
52+ return df [["symbol" , * REQUIRED_PRICE_COLUMNS ]]
53+
54+
55+ def _fetch_yfinance_daily (symbol ):
56+ ticker = yf .Ticker (symbol )
57+ df = ticker .history (period = "1y" , interval = "1d" , auto_adjust = False )
58+ return _normalize_prices (symbol , df )
59+
60+
61+ def fetch_candidate_prices (ingestor , symbol ):
62+ methods = [
63+ ("twelvedata" , lambda : ingestor .fetch_twelvedata_daily (symbol )),
64+ ("yfinance" , lambda : _fetch_yfinance_daily (symbol )),
65+ ("stooq" , lambda : ingestor .fetch_stooq_data (symbol )),
66+ ]
67+ errors = []
68+ for source_name , loader in methods :
69+ try :
70+ df = loader ()
71+ except Exception as exc :
72+ errors .append (f"{ source_name } :{ exc } " )
73+ continue
74+ df = _normalize_prices (symbol , df )
75+ if df is not None and not df .empty :
76+ return df , source_name , None
77+ errors .append (f"{ source_name } :empty" )
78+ return None , None , "; " .join (errors )
79+
80+
1781def compute_candidate (symbol , prices_df ):
1882 df = prices_df .copy ()
1983 if df .empty or len (df ) < 60 :
@@ -80,14 +144,15 @@ def build_candidates(config, tickers):
80144 candidates = []
81145 failures = []
82146 for symbol in tickers :
83- df = ingestor . fetch_stooq_data ( symbol )
147+ df , source_name , error = fetch_candidate_prices ( ingestor , symbol )
84148 if df is None or df .empty :
85- failures .append ({"symbol" : symbol , "error" : "no_price_data" })
149+ failures .append ({"symbol" : symbol , "error" : error or "no_price_data" })
86150 continue
87151 candidate = compute_candidate (symbol , df )
88152 if candidate is None :
89- failures .append ({"symbol" : symbol , "error" : "insufficient_history" })
153+ failures .append ({"symbol" : symbol , "error" : f "insufficient_history: { source_name } " })
90154 continue
155+ candidate ["price_source" ] = source_name
91156 candidates .append (candidate )
92157 return candidates , failures
93158
@@ -113,6 +178,7 @@ def main():
113178 "candidate_failures" : failures ,
114179 "status" : status ,
115180 "trades" : trades ,
181+ "price_sources" : {c ["symbol" ]: c .get ("price_source" ) for c in candidates },
116182 }
117183
118184 os .makedirs ("results" , exist_ok = True )
0 commit comments