Skip to content

Commit 2e29613

Browse files
committed
Stabilize ThetaData chain tests under quiet logging
1 parent c94531e commit 2e29613

File tree

2 files changed

+59
-7
lines changed

2 files changed

+59
-7
lines changed

lumibot/tools/thetadata_helper.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,6 +1396,7 @@ def check_connection(username: str, password: str, wait_for_connection: bool = F
13961396
max_retries = CONNECTION_MAX_RETRIES
13971397
sleep_interval = CONNECTION_RETRY_SLEEP
13981398
restart_attempts = 0
1399+
proactive_restart_attempts = 0
13991400
client = None
14001401

14011402
def probe_status() -> Optional[str]:
@@ -1464,6 +1465,23 @@ def probe_status() -> Optional[str]:
14641465
counter += 1
14651466
if counter % 10 == 0:
14661467
logger.info("Waiting for ThetaTerminal connection (attempt %s/%s).", counter, max_retries)
1468+
if counter and counter % 15 == 0:
1469+
if proactive_restart_attempts >= MAX_RESTART_ATTEMPTS:
1470+
logger.error(
1471+
"ThetaTerminal still disconnected after %s attempts; restart limit reached.",
1472+
counter,
1473+
)
1474+
break
1475+
proactive_restart_attempts += 1
1476+
logger.warning(
1477+
"ThetaTerminal disconnected for %s consecutive probes; restarting (proactive #%s).",
1478+
counter,
1479+
proactive_restart_attempts,
1480+
)
1481+
client = start_theta_data_client(username=username, password=password)
1482+
time.sleep(max(BOOT_GRACE_PERIOD, sleep_interval))
1483+
counter = 0
1484+
continue
14671485
time.sleep(sleep_interval)
14681486

14691487
if not connected and counter >= max_retries:
@@ -1476,6 +1494,8 @@ def get_request(url: str, headers: dict, querystring: dict, username: str, passw
14761494
all_responses = []
14771495
next_page_url = None
14781496
page_count = 0
1497+
consecutive_disconnects = 0
1498+
restart_budget = 3
14791499

14801500
# Lightweight liveness probe before issuing the request
14811501
check_connection(username=username, password=password, wait_for_connection=False)
@@ -1498,25 +1518,51 @@ def get_request(url: str, headers: dict, querystring: dict, username: str, passw
14981518
)
14991519

15001520
response = requests.get(request_url, headers=headers, params=request_params)
1521+
status_code = response.status_code
15011522
# Status code 472 means "No data" - this is valid, return None
1502-
if response.status_code == 472:
1523+
if status_code == 472:
15031524
logger.warning(f"No data available for request: {response.text[:200]}")
15041525
# DEBUG-LOG: API response - no data
15051526
logger.debug(
15061527
"[THETA][DEBUG][API][RESPONSE] status=472 result=NO_DATA"
15071528
)
1529+
consecutive_disconnects = 0
15081530
return None
1531+
elif status_code == 474:
1532+
consecutive_disconnects += 1
1533+
logger.warning("Received 474 from Theta Data (attempt %s): %s", counter + 1, response.text[:200])
1534+
if consecutive_disconnects >= 2:
1535+
if restart_budget <= 0:
1536+
logger.error("Restart budget exhausted after repeated 474 responses.")
1537+
raise ValueError("Cannot connect to Theta Data!")
1538+
logger.warning(
1539+
"Restarting ThetaTerminal after %s consecutive 474 responses (restart budget remaining %s).",
1540+
consecutive_disconnects,
1541+
restart_budget - 1,
1542+
)
1543+
restart_budget -= 1
1544+
start_theta_data_client(username=username, password=password)
1545+
check_connection(username=username, password=password, wait_for_connection=True)
1546+
time.sleep(max(BOOT_GRACE_PERIOD, CONNECTION_RETRY_SLEEP))
1547+
consecutive_disconnects = 0
1548+
counter = 0
1549+
else:
1550+
check_connection(username=username, password=password, wait_for_connection=True)
1551+
time.sleep(CONNECTION_RETRY_SLEEP)
1552+
continue
15091553
# If status code is not 200, then we are not connected
1510-
elif response.status_code != 200:
1511-
logger.warning(f"Non-200 status code {response.status_code}: {response.text[:200]}")
1554+
elif status_code != 200:
1555+
logger.warning(f"Non-200 status code {status_code}: {response.text[:200]}")
15121556
# DEBUG-LOG: API response - error
15131557
logger.debug(
15141558
"[THETA][DEBUG][API][RESPONSE] status=%d result=ERROR",
1515-
response.status_code
1559+
status_code
15161560
)
15171561
check_connection(username=username, password=password, wait_for_connection=True)
1562+
consecutive_disconnects = 0
15181563
else:
15191564
json_resp = response.json()
1565+
consecutive_disconnects = 0
15201566

15211567
# DEBUG-LOG: API response - success
15221568
response_rows = len(json_resp.get("response", [])) if isinstance(json_resp.get("response"), list) else 0

tests/test_thetadata_helper.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,8 +1426,10 @@ def test_chains_cached_handles_none_builder(self, tmp_path, monkeypatch, caplog)
14261426

14271427
monkeypatch.setattr(thetadata_helper, "build_historical_chain", lambda **kwargs: None)
14281428
monkeypatch.setattr(thetadata_helper, "LUMIBOT_CACHE_FOLDER", str(tmp_path))
1429+
monkeypatch.delenv("BACKTESTING_QUIET_LOGS", raising=False)
1430+
caplog.set_level(logging.WARNING, logger="lumibot.tools.thetadata_helper")
14291431

1430-
with caplog.at_level(logging.WARNING):
1432+
with caplog.at_level(logging.WARNING, logger="lumibot.tools.thetadata_helper"):
14311433
result = thetadata_helper.get_chains_cached("user", "pass", asset, test_date)
14321434

14331435
cache_folder = Path(tmp_path) / "thetadata" / "stock" / "option_chains"
@@ -1508,8 +1510,10 @@ def fake_get_request(url, headers, querystring, username, password):
15081510
raise AssertionError(f"Unexpected URL {url}")
15091511

15101512
monkeypatch.setattr(thetadata_helper, "get_request", fake_get_request)
1513+
monkeypatch.delenv("BACKTESTING_QUIET_LOGS", raising=False)
1514+
caplog.set_level(logging.WARNING, logger="lumibot.tools.thetadata_helper")
15111515

1512-
with caplog.at_level(logging.WARNING):
1516+
with caplog.at_level(logging.WARNING, logger="lumibot.tools.thetadata_helper"):
15131517
result = thetadata_helper.build_historical_chain("user", "pass", asset, as_of_date)
15141518

15151519
assert result is None
@@ -1525,8 +1529,10 @@ def fake_get_request(url, headers, querystring, username, password):
15251529
raise AssertionError("Unexpected call after empty expirations")
15261530

15271531
monkeypatch.setattr(thetadata_helper, "get_request", fake_get_request)
1532+
monkeypatch.delenv("BACKTESTING_QUIET_LOGS", raising=False)
1533+
caplog.set_level(logging.WARNING, logger="lumibot.tools.thetadata_helper")
15281534

1529-
with caplog.at_level(logging.WARNING):
1535+
with caplog.at_level(logging.WARNING, logger="lumibot.tools.thetadata_helper"):
15301536
result = thetadata_helper.build_historical_chain("user", "pass", asset, as_of_date)
15311537

15321538
assert result is None

0 commit comments

Comments
 (0)