From e2af2eff792c87ed63478792ce196dff6a9f17a0 Mon Sep 17 00:00:00 2001 From: wangwei0518 <1329996666@qq.com> Date: Sat, 6 Jun 2026 13:04:47 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20NDX=20=E6=97=A5=E6=9C=9F?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=92=8C=E7=BD=91=E7=BB=9C=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NdxDailyReport/config.example.yaml | 4 ++ NdxDailyReport/ndx_daily_report.py | 33 +++++++++++++-- NdxDailyReport/test_ndx_daily_report.py | 54 +++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 NdxDailyReport/test_ndx_daily_report.py diff --git a/NdxDailyReport/config.example.yaml b/NdxDailyReport/config.example.yaml index 308a6ce..b1d19b4 100644 --- a/NdxDailyReport/config.example.yaml +++ b/NdxDailyReport/config.example.yaml @@ -16,6 +16,10 @@ network: proxy_url: "" # auto 会在 https 代理或环境代理场景下使用 curl。 transport: auto + # curl 经代理访问时默认使用 HTTP/1.1,并对瞬时网络错误重试 2 次。 + curl_http1_1: true + curl_retry_count: 2 + curl_retry_delay_seconds: 1 qqbot: appid: "用户填写" diff --git a/NdxDailyReport/ndx_daily_report.py b/NdxDailyReport/ndx_daily_report.py index 1283bb7..d11e9f3 100644 --- a/NdxDailyReport/ndx_daily_report.py +++ b/NdxDailyReport/ndx_daily_report.py @@ -363,16 +363,29 @@ def parse_trade_date(timestamp: str, timezone_name: str) -> str: def parse_trade_datetime(timestamp: str, timezone_name: str) -> datetime: 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) - 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: 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) except ValueError: continue 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: raise ValueError(f"无法解析 lastTradeTimestamp: {timestamp}") from exc @@ -614,6 +627,20 @@ def curl_request( "--request", 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() if proxy_url: command.extend(["--proxy", proxy_url]) diff --git a/NdxDailyReport/test_ndx_daily_report.py b/NdxDailyReport/test_ndx_daily_report.py new file mode 100644 index 0000000..902279e --- /dev/null +++ b/NdxDailyReport/test_ndx_daily_report.py @@ -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()