본문 바로가기
C.W.K.
Stream
Lesson 02 of 05 · published

계산하지 말고 물어봐

~12 min · provider, timezone, api-design, fallback

Level 0원석
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
장 날짜는 제공자가 이미 들고 있는 사실이다. 그걸 유도하려는 시도는 전부 남의 달력을 다시 구현하는 짓이다.

날짜를 얻는 세 가지 방법, 순위대로

기계 달력이 시장 달력이 아니라는 걸 받아들이고 나면 후보 수선책이 셋인데, 세 번째만 버텨.

고정 오프셋을 뺀다. "뉴욕은 UTC-5 니까 날짜 뽑기 전에 다섯 시간 빼자." 일 년에 두 번, 양방향으로 틀려. 그리고 그 실패가 딱 아무도 안 보는 자리에 몰려. 서머타임 전환 다음 주, 그것도 한 시장에서만.

거래소 달력을 조회한다. 낫지. 그리고 이제 세 나라 거래소 개장 시간이랑 휴장일 테이블을 소유하게 됐고, 그건 흘러가는 두 번째 진실 원본이야. 흘러갔다는 건 어느 시장이 예정에 없던 반장을 하는 날 알게 되고.

제공자한테 묻는다. 시세가 이미 찍힌 순간이랑 거래소 시간대 이름을 달고 있어. 하나를 다른 하나로 통과시키면 거래소 자신이 쓸 장 날짜가 나와. 테이블도, 오프셋도, 표류도 없어. 답이 그게 설명하는 값이랑 같은 데서 오니까.

들을 수 있는 건 유도하지 마. 페이로드에 사실이 들어 있으면 그 사실을 받아. 유도는 전부 남의 규칙을 두 번째로 구현하는 거고, 그 규칙이 흥미로워지는 바로 그날 — 휴장일, 전환일, 예정에 없던 폐장 — 저쪽이랑 갈라져. 유도 판은 코드가 더 많기만 한 게 아니라, 하필 중요한 순간에 틀리는 코드가 더 많은 거야.

구현이 실제로 하는 일

함수는 짧고, 재밌는 건 가장자리야. 필드 둘을 읽어 — 마지막 정규장 순간의 에포크 초 타임스탬프랑 거래소 시간대 이름 — UTC 로 aware datetime 을 만들고, 거래소 존으로 변환하고, 날짜를 뽑아.

서버 존으로 변환하지 않는다는 데 주목해. 중간 표현이 UTC 인 건 모호하지 않아서고, 목적지는 거래소야. 이게 무슨 장인지 정의하는 게 거래소니까.

정직함이 사는 자리는 폴백이야

제공자가 조용하면 함수는 None 을 돌려줘. 타임스탬프가 없거나, 존 이름이 없거나, 파싱이 안 되는 경우. 추측을 안 해. 그리고 독스트링은 그러면 호출자가 UTC 날짜로 폴백하되 그렇다고 말한다고 적어놨어. 달력을 지어내는 대신.

그 마지막 절이 이 규율 전체의 축소판이야. 진짜랑 구분이 안 되는 폴백은 폴백이 아니라 조용한 하향이고, 흔적을 안 남기는 하향이 바로 시스템 정확도가 언제 그랬는지 아무도 못 짚는 채로 닳아가는 방식이야.

이 레슨을 처음 쓸 때, 그 절은 구현이 안 돼 있었어. 호출자가 폴백 날짜를 집어넣고 제공자 원래 출처 문자열을 그대로 저장했거든. 그래서 폴백으로 날짜 붙은 행이 제공자가 날짜를 준 행이랑 출처가 바이트 단위로 똑같았어. 추측 대신 None 을 돌려주는 정직한 절반은 진짜였고 하중도 받고 있었는데, 공개하는 절반은 독스트링에만 살았지. 코드가 뭘 하는지 설명하는 주석은 증거가 아니라 주장이다. 그 주석은 인용되고 믿기고 되풀이돼 왔어. 이 레슨의 이전 판까지 포함해서.

그리고 2026-08-31 에 닫혔어

닫는 게 어떻게 생겼는지가 틈 자체보다 배울 게 많아. 고친 사람이 폴백 날짜 를 건드린 게 아니야. 그건 원래 맞았거든. 고친 건 그 옆에 나가는 출처 문자열 이야. 두 호출 경로가 각자 들고 있던 폴백 헬퍼를 없애고, 갈림길 하나를 함수로 만들어서 둘 다 그걸 통과하게 했어. 제공자가 날짜를 말했으면 출처는 손 안 대고 그대로. 안 말했으면 UTC 날짜에 (date:utc-fallback) 를 붙여서 나가. 그게 전부야.

세 가지가 눈에 띄어야 해. 첫째, 고침이 저장 계층까지 내려갔어. 로그 줄이나 메트릭이 아니라 행 자체가 달라졌어. 이 창고의 불변식이 저장하는 모든 값에 출처 인데, 날짜가 둘 중 어느 쪽이냐는 게 바로 출처거든. 둘째, 갈림길이 하나로 모였어. 두 군데서 각자 폴백하고 있었으니 같은 결함이 두 벌 있었던 거고, 한 군데만 고쳤으면 나머지 하나가 조용히 남았을 거야. 셋째, 회귀 테스트가 양쪽 분기를 다 못박았어. 제공자가 날짜를 준 경우엔 출처가 안 붙는다는 것까지. 태그가 붙는지만 검사하는 테스트는 전부 태그를 붙여버리는 구현도 통과시키니까.

제일 좋은 신호는 그 틈이 어떻게 발견됐냐야. 정적 분석이 잡은 게 아니야. 타입도 통과하고 테스트도 통과했거든. 이 레슨이 잡았어. 누가 코드베이스를 가르치려고 앉아서 독스트링이 약속한 걸 적고 호출자가 진짜 그러는지 확인하러 갔다가 발견한 거야. 설명은 검증의 한 형태야. 그리고 어느 시스템에 대해서든 제일 값싼 감사는, 그게 자기에 대해 뭐라고 말하는지 적어놓고 한 줄씩 대조해보는 거야.
좁게 잡고, 하나하나 왜 있는지 알아. 이 변환은 구체적인 오류 넷을 잡아. 범위 밖 타임스탬프, 오버플로, 잘못된 값, 모르는 시간대 이름. 누가 생각하고 만든 목록이지 맨 except 가 아니야. 항목마다 제공자가 실제로 보낼 수 있는 것에 대응하고, 좁게 잡았으니까 진짜 예상 못 한 실패는 None 으로 뭉개지지 않고 여전히 드러나.

Code

장 날짜는 물어봐서 받고, 못 받으면 그렇다고 말하기·python
def listing_close_date(info: dict[str, Any]) -> str | None:
    """The session date a quote actually belongs to -- asked of the
    provider, never computed from this machine's clock.

    Measured 2026-08-06 05:49 UTC: a US ETF proxy -> 2026-08-05
    (New York), a Tokyo-listed ETF -> 2026-08-06 (Tokyo). Two
    different session dates at one instant, which is exactly why a
    single local date cannot be right for both.

    Returns None when the provider is silent -- callers then fall
    back to the UTC date and say so -- see `dated_and_sourced`,
    which is what makes "say so" true rather than aspirational."""
    stamp = info.get("regularMarketTime")      # epoch seconds
    zone = info.get("exchangeTimezoneName")    # e.g. "America/New_York"
    if not isinstance(stamp, (int, float)) or not zone:
        return None
    try:
        moment = datetime.datetime.fromtimestamp(
            float(stamp), datetime.UTC)
        return moment.astimezone(
            zoneinfo.ZoneInfo(str(zone))).date().isoformat()
    except (OSError, OverflowError, ValueError,
            zoneinfo.ZoneInfoNotFoundError):
        return None


# The seam that closed the gap, 2026-08-31. Both ingest paths used to
# keep a private `_fallback_date()` and store `result["source"]`
# unchanged either way; both now go through here instead. The date was
# never the bug -- the source string beside it was.
UTC_FALLBACK_TAG = "(date:utc-fallback)"


def dated_and_sourced(result: dict[str, Any]) -> tuple[str, str]:
    """The (data_date, source) pair to store for one provider result."""
    provider_date = result.get("data_date")
    if provider_date:
        return str(provider_date), str(result["source"])
    fallback = datetime.datetime.now(datetime.UTC).date().isoformat()
    return fallback, f"{result['source']} {UTC_FALLBACK_TAG}"


# And the half of the test that is easy to forget: a machine-dated row
# must be tagged, AND a market-dated one must NOT be. Assert only the
# first and an implementation that tags everything still passes.
assert dated_and_sourced(
    {"source": "polygon:AAPL", "data_date": "2026-08-28"}
) == ("2026-08-28", "polygon:AAPL")

External links

Exercise

상류 페이로드가 직접 알려줄 수 있었는데 네 코드가 유도하는 값을 하나 찾아봐. 날짜든 통화든 단위든 상태든. 유도를 그 필드로 갈아끼우고, 필드가 없는 경우엔 네 유도로 흘러가지 말고 명시적인 '모름' 을 돌려주게 해. 그다음 어떤 호출자가 그 '모름' 을 조용히 덮고 있는지 확인해. 진짜 버그는 보통 거기 숨어 있어.
Hint
표시는 남의 정책을 인코딩한 네 코드 속 상수야. 오프셋, 장 마감 시각, 반올림 규칙, 통화 가정. 하나하나가 권위 있는 어딘가에 사는 사실의 사본이고, 사본은 네 일정이 아니라 원본 일정에 맞춰 낡아.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 피파— warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.