修复 NDX 日期解析和网络重试
This commit is contained in:
@@ -16,6 +16,10 @@ network:
|
|||||||
proxy_url: ""
|
proxy_url: ""
|
||||||
# auto 会在 https 代理或环境代理场景下使用 curl。
|
# auto 会在 https 代理或环境代理场景下使用 curl。
|
||||||
transport: auto
|
transport: auto
|
||||||
|
# curl 经代理访问时默认使用 HTTP/1.1,并对瞬时网络错误重试 2 次。
|
||||||
|
curl_http1_1: true
|
||||||
|
curl_retry_count: 2
|
||||||
|
curl_retry_delay_seconds: 1
|
||||||
|
|
||||||
qqbot:
|
qqbot:
|
||||||
appid: "用户填写"
|
appid: "用户填写"
|
||||||
|
|||||||
@@ -363,16 +363,29 @@ def parse_trade_date(timestamp: str, timezone_name: str) -> str:
|
|||||||
|
|
||||||
def parse_trade_datetime(timestamp: str, timezone_name: str) -> datetime:
|
def parse_trade_datetime(timestamp: str, timezone_name: str) -> datetime:
|
||||||
cleaned = timestamp.replace("ET", "").strip()
|
cleaned = timestamp.replace("ET", "").strip()
|
||||||
formats = ["%b %d, %Y %I:%M %p", "%m/%d/%Y %I:%M %p", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d"]
|
|
||||||
tz = get_timezone(timezone_name)
|
tz = get_timezone(timezone_name)
|
||||||
for fmt in formats:
|
formats = [
|
||||||
|
("%b %d, %Y %I:%M %p", False),
|
||||||
|
("%m/%d/%Y %I:%M %p", False),
|
||||||
|
("%Y-%m-%d %H:%M:%S", False),
|
||||||
|
("%b %d, %Y", True),
|
||||||
|
("%m/%d/%Y", True),
|
||||||
|
("%Y-%m-%d", True),
|
||||||
|
]
|
||||||
|
for fmt, date_only in formats:
|
||||||
try:
|
try:
|
||||||
dt = datetime.strptime(cleaned, fmt)
|
dt = datetime.strptime(cleaned, fmt)
|
||||||
|
if date_only:
|
||||||
|
# Nasdaq occasionally returns only the date for an index close.
|
||||||
|
dt = datetime.combine(dt.date(), datetime_time(16, 0))
|
||||||
return dt.replace(tzinfo=tz)
|
return dt.replace(tzinfo=tz)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
return datetime.fromisoformat(cleaned).astimezone(tz)
|
dt = datetime.fromisoformat(cleaned)
|
||||||
|
if dt.tzinfo is None:
|
||||||
|
return dt.replace(tzinfo=tz)
|
||||||
|
return dt.astimezone(tz)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise ValueError(f"无法解析 lastTradeTimestamp: {timestamp}") from exc
|
raise ValueError(f"无法解析 lastTradeTimestamp: {timestamp}") from exc
|
||||||
|
|
||||||
@@ -614,6 +627,20 @@ def curl_request(
|
|||||||
"--request",
|
"--request",
|
||||||
method,
|
method,
|
||||||
]
|
]
|
||||||
|
if bool(proxy_config.get("curl_http1_1", True)):
|
||||||
|
# Some HTTPS proxies intermittently fail while tunnelling HTTP/2.
|
||||||
|
command.append("--http1.1")
|
||||||
|
retry_count = max(0, int(proxy_config.get("curl_retry_count", 2)))
|
||||||
|
if retry_count and method.upper() in {"GET", "HEAD"}:
|
||||||
|
command.extend(
|
||||||
|
[
|
||||||
|
"--retry",
|
||||||
|
str(retry_count),
|
||||||
|
"--retry-delay",
|
||||||
|
str(max(0, int(proxy_config.get("curl_retry_delay_seconds", 1)))),
|
||||||
|
"--retry-all-errors",
|
||||||
|
]
|
||||||
|
)
|
||||||
proxy_url = str(proxy_config.get("proxy_url", "")).strip()
|
proxy_url = str(proxy_config.get("proxy_url", "")).strip()
|
||||||
if proxy_url:
|
if proxy_url:
|
||||||
command.extend(["--proxy", proxy_url])
|
command.extend(["--proxy", proxy_url])
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import subprocess
|
||||||
|
import unittest
|
||||||
|
from datetime import time
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import ndx_daily_report as report
|
||||||
|
|
||||||
|
|
||||||
|
class ParseTradeDatetimeTests(unittest.TestCase):
|
||||||
|
def test_nasdaq_date_only_timestamp_represents_market_close(self) -> None:
|
||||||
|
parsed = report.parse_trade_datetime("Jun 5, 2026", "America/New_York")
|
||||||
|
|
||||||
|
self.assertEqual(parsed.date().isoformat(), "2026-06-05")
|
||||||
|
self.assertEqual(parsed.time(), time(16, 0))
|
||||||
|
|
||||||
|
def test_naive_iso_timestamp_uses_trade_timezone(self) -> None:
|
||||||
|
parsed = report.parse_trade_datetime("2026-06-05T16:01:00", "America/New_York")
|
||||||
|
|
||||||
|
self.assertEqual(parsed.hour, 16)
|
||||||
|
self.assertIsNotNone(parsed.tzinfo)
|
||||||
|
|
||||||
|
def test_date_only_timestamp_passes_closed_market_validation(self) -> None:
|
||||||
|
parsed = report.parse_trade_datetime("Jun 5, 2026", "America/New_York")
|
||||||
|
|
||||||
|
report.validate_complete_regular_close(parsed, "Closed", "America/New_York")
|
||||||
|
|
||||||
|
|
||||||
|
class CurlRequestTests(unittest.TestCase):
|
||||||
|
@patch("ndx_daily_report.subprocess.run")
|
||||||
|
@patch("ndx_daily_report.shutil.which", return_value="/usr/bin/curl")
|
||||||
|
def test_curl_uses_http1_and_retries_by_default(self, _which, run) -> None:
|
||||||
|
run.return_value = subprocess.CompletedProcess([], 0, stdout=b"ok", stderr=b"")
|
||||||
|
|
||||||
|
result = report.curl_request("GET", "https://example.com", None, 15, None, {})
|
||||||
|
|
||||||
|
self.assertEqual(result, b"ok")
|
||||||
|
command = run.call_args.args[0]
|
||||||
|
self.assertIn("--http1.1", command)
|
||||||
|
self.assertEqual(command[command.index("--retry") + 1], "2")
|
||||||
|
self.assertIn("--retry-all-errors", command)
|
||||||
|
|
||||||
|
@patch("ndx_daily_report.subprocess.run")
|
||||||
|
@patch("ndx_daily_report.shutil.which", return_value="/usr/bin/curl")
|
||||||
|
def test_curl_does_not_retry_post_requests(self, _which, run) -> None:
|
||||||
|
run.return_value = subprocess.CompletedProcess([], 0, stdout=b"{}", stderr=b"")
|
||||||
|
|
||||||
|
report.curl_request("POST", "https://example.com", {"value": 1}, 15, None, {})
|
||||||
|
|
||||||
|
command = run.call_args.args[0]
|
||||||
|
self.assertNotIn("--retry", command)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user