mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-11-03 16:12:44 +02:00
- the bar of a session still trading, by its market's date, is not scored - a holding window that has passed without the stock or its benchmark trading through is fetched again through today - a window that cannot have traded yet asks for no prices
This commit is contained in:
@@ -1138,6 +1138,75 @@ class TestSettlementWindow:
|
||||
assert alpha == pytest.approx(raw - (spy["2026-01-14"] / spy["2026-01-07"] - 1))
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSettlementUsesFinalCloses:
|
||||
"""A decision settles on closes that are final, whenever the pass runs."""
|
||||
|
||||
def test_a_bar_of_a_session_still_trading_is_not_scored(self, monkeypatch):
|
||||
# 09:12 in Shanghai on Tue 13: Yahoo already serves that day's bar at the live price.
|
||||
days = pd.bdate_range("2026-01-02", "2026-01-13", tz="Asia/Shanghai")
|
||||
stock = pd.Series([100.0 + i for i in range(len(days))], index=days)
|
||||
index = pd.Series([3000.0] * len(days), index=days)
|
||||
monkeypatch.setattr(settlement, "get_closes", _closes({"600519.SS": stock, "000001.SS": index}))
|
||||
monkeypatch.setattr(settlement, "_now", lambda: pd.Timestamp("2026-01-13 01:12", tz="UTC"))
|
||||
|
||||
# Decided Mon 5: entry Tue 6, exit five sessions later on Tue 13, today.
|
||||
assert settlement.fetch_returns("600519.SS", "2026-01-05", 5, "000001.SS") == (None, None, None, None)
|
||||
|
||||
monkeypatch.setattr(settlement, "_now", lambda: pd.Timestamp("2026-01-14 01:12", tz="UTC"))
|
||||
assert settlement.fetch_returns("600519.SS", "2026-01-05", 5, "000001.SS")[3] == "2026-01-13"
|
||||
|
||||
def test_a_decision_before_a_long_market_closure_settles(self, monkeypatch):
|
||||
# The exchange is shut Thu 1 to Wed 7 October.
|
||||
days = pd.bdate_range("2026-09-21", "2026-10-30")
|
||||
days = days[(days < "2026-10-01") | (days > "2026-10-07")]
|
||||
stock = pd.Series([100.0 + i for i in range(len(days))], index=days)
|
||||
index = pd.Series([3000.0 + i for i in range(len(days))], index=days)
|
||||
monkeypatch.setattr(settlement, "get_closes", _closes({"600519.SS": stock, "000001.SS": index}))
|
||||
monkeypatch.setattr(settlement, "_now", lambda: pd.Timestamp("2026-11-02 08:00", tz="UTC"))
|
||||
|
||||
raw, alpha, days_held, resolved = settlement.fetch_returns("600519.SS", "2026-09-29", 5, "000001.SS")
|
||||
|
||||
# Entry Wed 30 September, then five sessions from Thu 8 October.
|
||||
assert resolved == "2026-10-14" and days_held == 5
|
||||
|
||||
def test_a_market_whose_clocks_skip_midnight_still_settles(self, monkeypatch):
|
||||
# Santiago springs forward at midnight: 2026-09-06 has no 00:00.
|
||||
days = pd.bdate_range("2026-08-21", "2026-09-04", tz="America/Santiago")
|
||||
stock = pd.Series([100.0 + i for i in range(len(days))], index=days)
|
||||
index = pd.Series([6000.0 + i for i in range(len(days))], index=days)
|
||||
monkeypatch.setattr(settlement, "get_closes", _closes({"SQM-B.SN": stock, "^IPSA": index}))
|
||||
monkeypatch.setattr(settlement, "_now", lambda: pd.Timestamp("2026-09-06 15:00", tz="UTC"))
|
||||
|
||||
assert settlement.fetch_returns("SQM-B.SN", "2026-08-24", 5, "^IPSA")[3] == "2026-09-01"
|
||||
|
||||
def test_a_benchmark_shut_past_the_window_is_fetched_again(self, monkeypatch):
|
||||
days = pd.bdate_range("2026-09-14", "2026-10-30")
|
||||
stock = pd.Series([100.0 + i for i in range(len(days))], index=days)
|
||||
shut = days[(days < "2026-09-30") | (days > "2026-10-20")]
|
||||
index = pd.Series([3000.0 + i for i in range(len(shut))], index=shut)
|
||||
monkeypatch.setattr(settlement, "get_closes", _closes({"AAPL": stock, "000001.SS": index}))
|
||||
monkeypatch.setattr(settlement, "_now", lambda: pd.Timestamp("2026-11-02 08:00", tz="UTC"))
|
||||
|
||||
assert settlement.fetch_returns("AAPL", "2026-09-29", 5, "000001.SS")[3] == "2026-10-07"
|
||||
|
||||
def test_a_series_with_no_usable_close_stays_pending(self, monkeypatch):
|
||||
# Yahoo can answer with a dated, time-zoned frame whose closes are all missing.
|
||||
days = pd.bdate_range("2026-01-02", "2026-01-30", tz="America/New_York")
|
||||
empty = pd.Series([float("nan")] * len(days), index=days)
|
||||
index = pd.Series([400.0 + i for i in range(len(days))], index=days)
|
||||
monkeypatch.setattr(settlement, "get_closes", _closes({"NVDA": empty, "SPY": index}))
|
||||
monkeypatch.setattr(settlement, "_now", lambda: pd.Timestamp("2026-02-02 12:00", tz="UTC"))
|
||||
|
||||
assert settlement.fetch_returns("NVDA", "2026-01-05", 5, "SPY") == (None, None, None, None)
|
||||
|
||||
def test_a_window_that_cannot_have_traded_asks_for_no_prices(self, monkeypatch):
|
||||
monkeypatch.setattr(settlement, "_now", lambda: pd.Timestamp("2026-01-08 22:00", tz="UTC"))
|
||||
monkeypatch.setattr(settlement, "get_closes", lambda *a: pytest.fail("fetched prices"))
|
||||
|
||||
assert settlement.fetch_returns("NVDA", "2026-01-05", 5, "SPY") == (None, None, None, None)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_close_that_is_not_a_price_is_skipped_not_scored(monkeypatch):
|
||||
"""A zero close would score as an infinite return and be written to the memory log."""
|
||||
|
||||
@@ -5,6 +5,8 @@ import logging
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from tradingagents.dataflows.symbols import normalize_symbol
|
||||
from tradingagents.dataflows.vendors.yahoo.market import get_closes
|
||||
|
||||
@@ -35,18 +37,24 @@ def resolve_benchmark(ticker: str, config: dict) -> str:
|
||||
return benchmark_map.get("", "SPY")
|
||||
|
||||
|
||||
def _now() -> pd.Timestamp:
|
||||
return pd.Timestamp.now(tz="UTC")
|
||||
|
||||
|
||||
def _by_day(closes):
|
||||
"""Closes keyed by the calendar day of their bar, missing closes dropped.
|
||||
"""Final closes keyed by the calendar day of their bar, missing closes dropped.
|
||||
|
||||
Yahoo stamps a daily bar at midnight in its market's time zone (UTC for a
|
||||
coin, New York for SPY), so two series meet on their dates, not their
|
||||
instants. A missing or non-positive close is no price at all, not a bar to
|
||||
score on.
|
||||
score on. Today's bar, in its market's time zone, is the session still
|
||||
trading: its close is the live price, so it is not scored.
|
||||
"""
|
||||
closes = closes[closes > 0]
|
||||
if getattr(closes.index, "tz", None) is not None:
|
||||
closes = closes.tz_localize(None)
|
||||
return closes
|
||||
tz = getattr(closes.index, "tz", None)
|
||||
if not closes.empty:
|
||||
closes = closes[closes.index.date < _now().tz_convert(tz).date()]
|
||||
return closes if tz is None else closes.tz_localize(None)
|
||||
|
||||
|
||||
def fetch_returns(
|
||||
@@ -67,18 +75,32 @@ def fetch_returns(
|
||||
to report.
|
||||
"""
|
||||
start = datetime.strptime(trade_date, "%Y-%m-%d")
|
||||
today = _now().tz_localize(None).normalize()
|
||||
# The entry and holding_days more sessions all close after the decision date
|
||||
# and before today, with a day's slack for markets east of UTC: until then
|
||||
# the window cannot have traded and no prices are asked for.
|
||||
if (today - start).days < holding_days + 1:
|
||||
return None, None, None, None
|
||||
# holding_days counts trading days after the entry session, so ask for the
|
||||
# calendar span they occupy (about 7 for every 5) plus a week for holidays.
|
||||
end = start + timedelta(days=round((holding_days + 1) * 7 / 5) + 7)
|
||||
end_str = end.strftime("%Y-%m-%d")
|
||||
|
||||
# Closes for the instrument the analysis priced (XAUUSD -> GC=F, #984),
|
||||
# from the first session after the decision date.
|
||||
stock = _by_day(get_closes(ticker, trade_date, end_str))
|
||||
stock = stock[stock.index > start]
|
||||
# From a week earlier, so the benchmark has a close on or before entry.
|
||||
bench_start = (start - timedelta(days=7)).strftime("%Y-%m-%d")
|
||||
bench = _by_day(get_closes(benchmark, bench_start, end_str))
|
||||
|
||||
def closes(until):
|
||||
# Closes for the instrument the analysis priced (XAUUSD -> GC=F, #984),
|
||||
# from the first session after the decision date.
|
||||
stock = _by_day(get_closes(ticker, trade_date, until.strftime("%Y-%m-%d")))
|
||||
return stock[stock.index > start], _by_day(get_closes(benchmark, bench_start, until.strftime("%Y-%m-%d")))
|
||||
|
||||
def traded(stock, bench):
|
||||
return len(stock) > holding_days and not bench.empty and bench.index[-1] >= stock.index[holding_days]
|
||||
|
||||
stock, bench = closes(end)
|
||||
# A window that has passed without trading through spans a closure longer than
|
||||
# the week allowed for holidays (a national holiday, a halt): ask through today.
|
||||
if not traded(stock, bench) and end < today:
|
||||
stock, bench = closes(today + timedelta(days=1))
|
||||
|
||||
# Require the full holding window to have traded. A rerun before it has
|
||||
# leaves the entry pending to retry next run, rather than settling on a
|
||||
|
||||
Reference in New Issue
Block a user