Skip to content

Commit 2fd7b58

Browse files
axumweyaneclaude
andcommitted
Retrain models with 5y data, wire options chain fetcher into pipeline
- Download 5 years OHLCV (2021-01-01 to 2026-03-20) for 10 symbols into TimescaleDB ohlcv_bars table (1,309 trading days per symbol) - Retrain TFT Stocks (val_loss 0.0249), Forex (0.0039), Volatility (0.0416) on RTX 5090 GPU with full 5-year dataset - Add fetch_options_data() to paper trader: fetches live options chains via yfinance (7-60 DTE) and converts to DataFrame with Heston params - Route deep_surrogates and tdgf strategies to real options chain data instead of stock OHLCV (which caused 0-signal output) - FX ETF order mapping, $2000 position cap, and execution guardrail override from prior session retained Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8f8f475 commit 2fd7b58

3 files changed

Lines changed: 156 additions & 12 deletions

File tree

TFT-main/models/tft_forex.pth

128 KB
Binary file not shown.

TFT-main/models/tft_volatility.pth

-99.1 KB
Binary file not shown.

TFT-main/paper-trader/main.py

Lines changed: 156 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,95 @@ def fetch_fx_data(pairs: List[str], days: int = 200) -> pd.DataFrame:
766766
return pd.DataFrame(rows)
767767

768768

769+
# ============================================================
770+
# OPTIONS CHAIN FETCHER
771+
# ============================================================
772+
def fetch_options_data(symbols: List[str]) -> pd.DataFrame:
773+
"""Fetch options chains via yfinance and convert to DataFrame for options strategies.
774+
775+
Returns a DataFrame with columns needed by DeepSurrogate, TDGF, and VolArb:
776+
symbol, strike, spot, expiry, moneyness, tau, rate, implied_vol, delta,
777+
bid, ask, volume, open_interest, option_type, kappa, theta, sigma, rho, v0.
778+
"""
779+
try:
780+
today = date.today()
781+
rows = []
782+
for symbol in symbols:
783+
try:
784+
ticker = yfinance.Ticker(symbol)
785+
# Get spot price
786+
hist = ticker.history(period="1d")
787+
if hist.empty:
788+
continue
789+
spot = float(hist["Close"].iloc[-1])
790+
if spot <= 0:
791+
continue
792+
793+
expiry_strings = ticker.options
794+
if not expiry_strings:
795+
continue
796+
797+
for exp_str in expiry_strings:
798+
exp_date = datetime.strptime(exp_str, "%Y-%m-%d").date()
799+
dte = (exp_date - today).days
800+
if dte < 7 or dte > 60:
801+
continue
802+
tau = max(dte / 365.0, 1e-6)
803+
804+
try:
805+
chain = ticker.option_chain(exp_str)
806+
except Exception:
807+
continue
808+
809+
for side_df, opt_type in [(chain.calls, "call"), (chain.puts, "put")]:
810+
if side_df is None or side_df.empty:
811+
continue
812+
for _, row in side_df.iterrows():
813+
strike = float(row.get("strike", 0))
814+
if strike <= 0:
815+
continue
816+
iv = float(row.get("impliedVolatility", 0) or 0)
817+
rows.append({
818+
"symbol": symbol,
819+
"strike": strike,
820+
"spot": spot,
821+
"expiry": exp_date,
822+
"moneyness": strike / spot,
823+
"tau": tau,
824+
"dte": dte,
825+
"rate": 0.05,
826+
"implied_vol": iv,
827+
"bid": float(row.get("bid", 0) or 0),
828+
"ask": float(row.get("ask", 0) or 0),
829+
"last": float(row.get("lastPrice", 0) or 0),
830+
"volume": int(row.get("volume", 0) or 0),
831+
"open_interest": int(row.get("openInterest", 0) or 0),
832+
"option_type": opt_type,
833+
# Default Heston params (model will calibrate from IV)
834+
"kappa": 2.0,
835+
"theta": 0.04,
836+
"sigma": max(iv, 0.01) if iv > 0 else 0.20,
837+
"rho": -0.7,
838+
"v0": max(iv ** 2, 1e-4) if iv > 0 else 0.04,
839+
})
840+
except Exception as e:
841+
logger.debug("Options fetch failed for %s: %s", symbol, e)
842+
843+
if not rows:
844+
logger.info("No options data fetched")
845+
return pd.DataFrame()
846+
847+
df = pd.DataFrame(rows)
848+
logger.info(
849+
"Fetched options data: %d contracts across %d symbols",
850+
len(df), df["symbol"].nunique(),
851+
)
852+
return df
853+
except Exception as e:
854+
logger.error("Options data fetch failed: %s", e)
855+
return pd.DataFrame()
856+
857+
769858
# ============================================================
770859
# STRATEGY RUNNER (dynamic registry)
771860
# ============================================================
@@ -881,10 +970,14 @@ async def run_daily_pipeline(is_manual: bool = False):
881970
except Exception:
882971
pass
883972

884-
# 1. Fetch data
885-
stock_data = fetch_stock_data(TRADING_SYMBOLS)
973+
# 1. Fetch data (include FX ETFs for order execution)
974+
_FX_ETFS = ["FXE", "FXB", "FXY", "FXA", "FXC", "FXF"]
975+
stock_data = fetch_stock_data(TRADING_SYMBOLS + _FX_ETFS)
886976
fx_data = fetch_fx_data(FX_PAIRS)
887977

978+
# Fetch options chains for options strategies (deep_surrogates, tdgf, vol_arb)
979+
options_data = fetch_options_data(TRADING_SYMBOLS)
980+
888981
if stock_data.empty:
889982
logger.error("No stock data fetched, aborting")
890983
return
@@ -957,16 +1050,19 @@ async def run_daily_pipeline(is_manual: bool = False):
9571050

9581051
strategy_outputs = []
9591052
fx_strategies = {"fx_carry_trend", "fx_momentum", "fx_vol_breakout"}
1053+
options_strategies = {"deep_surrogates", "tdgf"}
9601054

9611055
for strat_name, strategy in strategy_map.items():
962-
# Use FX data for FX strategies, stock data otherwise
963-
input_data = (
964-
fx_data
965-
if strat_name in fx_strategies and not fx_data.empty
966-
else stock_data
967-
)
968-
if strat_name in fx_strategies and fx_data.empty:
969-
continue
1056+
# Route data: FX strategies get fx_data, options strategies get
1057+
# options_data (with stock_data fallback), others get stock_data
1058+
if strat_name in fx_strategies:
1059+
if fx_data.empty:
1060+
continue
1061+
input_data = fx_data
1062+
elif strat_name in options_strategies and not options_data.empty:
1063+
input_data = options_data
1064+
else:
1065+
input_data = stock_data
9701066

9711067
output = _run_strategy(strat_name, strategy, input_data, today)
9721068
if output is not None:
@@ -1220,6 +1316,11 @@ async def run_daily_pipeline(is_manual: bool = False):
12201316
current_holdings = {}
12211317

12221318
trades_executed = 0
1319+
logger.info(
1320+
"Order loop: %d target positions: %s",
1321+
len(target.positions),
1322+
[f"{p.symbol}:{p.target_weight:.4f}" for p in target.positions],
1323+
)
12231324
for pos in target.positions:
12241325
# GUARDRAIL: Execution failure rate check before each order
12251326
exec_health = exec_monitor.check()
@@ -1245,15 +1346,45 @@ async def run_daily_pipeline(is_manual: bool = False):
12451346
target_value = pos.target_weight * state.portfolio_value
12461347
current_qty = current_holdings.get(symbol, 0)
12471348

1248-
# Get latest price
1249-
sym_data = stock_data[stock_data["symbol"] == symbol]
1349+
# FX pair → CurrencyShares ETF mapping for Alpaca execution
1350+
_FX_ETF_MAP = {
1351+
"EURUSD": ("FXE", False), # long EURUSD = long FXE
1352+
"GBPUSD": ("FXB", False),
1353+
"AUDUSD": ("FXA", False),
1354+
"USDJPY": ("FXY", True), # long USDJPY = short FXY
1355+
"USDCAD": ("FXC", True),
1356+
"USDCHF": ("FXF", True),
1357+
}
1358+
1359+
is_fx = symbol in _FX_ETF_MAP
1360+
trade_symbol = symbol
1361+
flip_side = False
1362+
1363+
if is_fx:
1364+
etf_sym, flip_side = _FX_ETF_MAP[symbol]
1365+
trade_symbol = etf_sym
1366+
# Use ETF holdings for position diff
1367+
current_qty = current_holdings.get(etf_sym, 0)
1368+
1369+
# Get latest price (check stock_data first, then fx_data)
1370+
sym_data = stock_data[stock_data["symbol"] == trade_symbol]
1371+
if sym_data.empty and not fx_data.empty:
1372+
sym_data = fx_data[fx_data["symbol"] == symbol]
12501373
if sym_data.empty:
1374+
logger.debug("Skipping %s: no price data found", symbol)
12511375
continue
12521376
price = float(sym_data.sort_values("timestamp")["close"].iloc[-1])
12531377
if price <= 0:
12541378
continue
12551379

1380+
# Cap FX ETF positions at $2000 max
1381+
_FX_ETF_MAX_VALUE = 2000.0
1382+
if is_fx:
1383+
target_value = max(-_FX_ETF_MAX_VALUE, min(_FX_ETF_MAX_VALUE, target_value))
1384+
12561385
target_shares = int(target_value / price)
1386+
if is_fx and flip_side:
1387+
target_shares = -target_shares
12571388
diff = target_shares - current_qty
12581389

12591390
if abs(diff) < 1:
@@ -1262,6 +1393,19 @@ async def run_daily_pipeline(is_manual: bool = False):
12621393
side = "buy" if diff > 0 else "sell"
12631394
qty = abs(diff)
12641395

1396+
if is_fx:
1397+
symbol = trade_symbol # Submit order with ETF ticker
1398+
1399+
# Cap sell qty at current holdings to avoid Alpaca rejection
1400+
# (position reversals require closing first, then opening opposite)
1401+
if side == "sell" and current_qty > 0:
1402+
qty = min(qty, int(abs(current_qty)))
1403+
elif side == "buy" and current_qty < 0:
1404+
qty = min(qty, int(abs(current_qty)))
1405+
1406+
if qty < 1:
1407+
continue
1408+
12651409
# Circuit breaker re-check before each trade
12661410
if circuit_breaker is not None:
12671411
try:

0 commit comments

Comments
 (0)