fix(sector-radar): 支持当前上市股票资金流补拉

This commit is contained in:
yuxuanhui
2026-08-31 14:26:28 +08:00
parent 1cd7b5cb38
commit 2ffd0163f2
20 changed files with 1077 additions and 93 deletions
@@ -33,11 +33,12 @@ class TushareSourceError(RuntimeError):
class RequestCoordinator:
"""Coordinate retries and shared rate-limit cooling for one provider client.
"""Coordinate retries, rate-limit cooling, and optional request start spacing.
Normal requests are not serialized. Only a classified provider limit creates
a shared cooldown. Injectable time functions keep long cooldowns deterministic
in tests without coupling the coordinator to any business bounded context.
Provider calls execute outside the coordinator lock and may overlap. When a
positive request interval is configured, only their start times are serialized.
Injectable time functions keep waits deterministic in tests without coupling
the coordinator to any business bounded context.
"""
def __init__(
@@ -45,6 +46,7 @@ class RequestCoordinator:
*,
max_retries: int = 3,
backoff_seconds: float = 1.0,
request_interval_seconds: float = 0.0,
cooldown_seconds: Sequence[float] = DEFAULT_RATE_LIMIT_COOLDOWNS,
random_fn: Callable[[], float] = random.random,
clock: Callable[[], float] = time.monotonic,
@@ -56,6 +58,7 @@ class RequestCoordinator:
raise ValueError("cooldown_seconds must contain non-negative values")
self.max_retries = max(0, max_retries)
self.backoff_seconds = max(0.0, backoff_seconds)
self.request_interval_seconds = max(0.0, request_interval_seconds)
self.cooldown_seconds = cooldowns
self.random_fn = random_fn
self.clock = clock
@@ -63,6 +66,7 @@ class RequestCoordinator:
self.sleep_fn = sleep_fn or wait_fn
self._condition = threading.Condition()
self._cooldown_until = 0.0
self._next_request_start = 0.0
self._rate_limit_count = 0
@property
@@ -77,7 +81,7 @@ class RequestCoordinator:
last_error: BaseException | None = None
for attempt in range(self.max_retries + 1):
self._wait_for_cooldown(method_name)
self._wait_for_request_start(method_name)
try:
result = request()
except Exception as exc:
@@ -124,17 +128,29 @@ class RequestCoordinator:
return self.call(method_name, operation)
def _wait_for_cooldown(self, method_name: str) -> None:
def _wait_for_request_start(self, method_name: str) -> None:
"""Reserve one start slot after both shared wait deadlines have elapsed."""
while True:
with self._condition:
delay = self._cooldown_until - self.clock()
if delay <= 0:
return
logger.info(
"provider_rate_limit_wait method=%s wait_seconds=%.1f",
method_name,
delay,
)
now = self.clock()
start_at = max(self._cooldown_until, self._next_request_start)
delay = start_at - now
if delay <= 0:
self._next_request_start = now + self.request_interval_seconds
return
if start_at == self._cooldown_until:
logger.info(
"provider_rate_limit_wait method=%s wait_seconds=%.1f",
method_name,
delay,
)
else:
logger.debug(
"provider_request_interval_wait method=%s wait_seconds=%.3f",
method_name,
delay,
)
self.wait_fn(delay)
def _set_rate_limit_cooldown(self) -> float: