<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: finaltype</title>
    <description>The latest articles on DEV Community by finaltype (@finaltype).</description>
    <link>https://dev.to/finaltype</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4089379%2F857e3758-7018-4922-ae19-5a1bcaf96498.png</url>
      <title>DEV Community: finaltype</title>
      <link>https://dev.to/finaltype</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/finaltype"/>
    <language>en</language>
    <item>
      <title>[260822] 티스토리를 떠나 GitHub Pages + Dev.to로 - 발행 자동화 개편</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Sat, 22 Aug 2026 13:04:05 +0000</pubDate>
      <link>https://dev.to/finaltype/260822-tiseutorireul-ddeona-github-pages-devtoro-balhaeng-jadonghwa-gaepyeon-2285</link>
      <guid>https://dev.to/finaltype/260822-tiseutorireul-ddeona-github-pages-devtoro-balhaeng-jadonghwa-gaepyeon-2285</guid>
      <description>&lt;p&gt;&lt;em&gt;개발일지가 몇 주째 뜸했던 진짜 이유를 찾아 고치고, 발행 플랫폼 자체를 바꿨습니다&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;지난 개발일지 이후로 열흘 넘게 조용했습니다. 오늘 그 이유를 찾았고, 겸사겸사 이 블로그의 발행 방식 자체를 크게 바꿨습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  왜 조용했나
&lt;/h2&gt;

&lt;p&gt;개발일지는 매일 밤 자동으로 마무리되게 만들어 놨는데, 실제로는 7월 24일 이후로 결과물이 하나도 안 남고 있었습니다.&lt;/p&gt;

&lt;p&gt;원인을 찾아보니 실행 자체는 매일 밤 정상적으로 돌고 있었습니다. 다만 마지막 저장 단계가 권한 승인 대기에 막혀서, 아무도 지켜보지 않는 새벽 시간대에 조용히 아무것도 안 남기고 끝나는 일이 한 달 가까이 반복됐습니다.&lt;/p&gt;

&lt;p&gt;스킵한 날엔 최소한 "스킵했다"는 로그 한 줄이라도 남게 설계해 둔 덕분에 뒤늦게라도 이 공백을 알아챌 수 있었습니다. 자세한 경위는 &lt;a href="https://finaltype.github.io/quant-blog/postmortems/2026-08-22_silent-automation-failure/" rel="noopener noreferrer"&gt;별도 글&lt;/a&gt;(새 창)로 따로 정리했습니다.&lt;/p&gt;

&lt;p&gt;오늘 원인을 고쳤고, 대화로 미리 초안을 안 써둔 날에도 실제 작업 기록을 직접 조사해서 글을 쓰도록 자동화 자체를 더 튼튼하게 다시 짰습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  티스토리를 떠나기로 했다
&lt;/h2&gt;

&lt;p&gt;이 블로그는 애초에 티스토리 글쓰기 API가 오래전에 종료돼서 발행만은 늘 수동이었습니다. 초안 작성과 변환까지만 자동화하고, 실제 등록은 화면에서 직접 붙여넣는 방식이었습니다.&lt;/p&gt;

&lt;p&gt;오늘 이 구조를 아예 바꾸기로 했습니다. 신규 발행은 GitHub Pages(정적 사이트)와 Dev.to(개발자 커뮤니티) 두 곳으로 옮기고, 티스토리는 과거 글 아카이브로만 남겨두기로 했습니다.&lt;/p&gt;

&lt;p&gt;원래는 다른 블로깅 플랫폼도 후보였는데, 발행 API가 최근 유료 전환된 걸 확인하고 제외했습니다. 대신 무료로 쓸 수 있는 조합으로 정리했습니다.&lt;/p&gt;

&lt;p&gt;기존에 쌓아둔 글 전체를 오늘 하루 동안 영어로 번역해서 새 플랫폼으로 옮겼습니다. 앞으로는 매일 작업일지·매주 요약·이벤트성 사후분석까지 전부 같은 파이프라인이 한국어로 먼저 쓰고, 사이트 재빌드부터 Dev.to 발행까지 한 번에 처리하도록 배선을 마쳤습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  사후분석 시리즈도 새로 만들었다
&lt;/h2&gt;

&lt;p&gt;지금까지는 사고나 오판을 개발일지 안에서 한 항목으로 짧게만 다뤘습니다. 오늘부터는 원인 분석이 필요할 만큼 무게 있는 사건은 별도의 사후분석 글로 따로 떼어 쓰기로 했습니다.&lt;/p&gt;

&lt;p&gt;오늘 그 시리즈의 첫 편들을 여러 개 함께 올렸습니다. 위에서 언급한 자동화 공백 건 외에도, 하드웨어 판정이 며칠 새 뒤집혔던 일이나 검증 도구 자체의 사각지대를 다룬 글들입니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  trading 쪽 — GPU 벤치마크 재개를 완전자동화
&lt;/h2&gt;

&lt;p&gt;로컬 대형 모델을 여러 번 반복 실행해 성능을 비교하는 벤치마크가 하나 진행 중인데, 어제 GPU 자원을 다른 작업과 나눠 쓰다가 중간에 멈춰 있었습니다.&lt;/p&gt;

&lt;p&gt;오늘은 이 벤치마크를 사람이 계속 지켜보지 않아도 GPU가 비는 즉시 명령 한 줄로 이어서 돌릴 수 있도록 재개 절차를 문서 하나로 정리했습니다. 시작할 때 입력 데이터의 지문을 검증해서, 재개 시점에 다른 입력을 잘못 물고 도는 일이 없도록 안전장치도 함께 넣었습니다.&lt;/p&gt;

&lt;p&gt;같은 날, GPU를 쓰는 모듈 전체를 훑어서 어디를 더 줄일 수 있는지 검토하는 자료도 준비해 뒀습니다. 이건 다음 주에 다른 AI 자문을 받아 우선순위를 정할 예정입니다.&lt;/p&gt;




&lt;p&gt;돌아보면 오늘은 새 기능 하나보다, "왜 조용했는지" 그 자체를 파고든 하루였습니다. 원인을 찾고 나니 고치는 건 오래 걸리지 않았고, 그 김에 이 블로그를 앞으로 더 오래 무인으로 돌릴 수 있는 형태로 다시 짰습니다.&lt;/p&gt;

</description>
      <category>quant</category>
      <category>trading</category>
      <category>devlog</category>
    </item>
    <item>
      <title>"[260822] Leaving Tistory for GitHub Pages + Dev.to - Overhauling the Publishing Pipeline"</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Sat, 22 Aug 2026 13:04:03 +0000</pubDate>
      <link>https://dev.to/finaltype/260822-leaving-tistory-for-github-pages-devto-overhauling-the-publishing-pipeline-4bm6</link>
      <guid>https://dev.to/finaltype/260822-leaving-tistory-for-github-pages-devto-overhauling-the-publishing-pipeline-4bm6</guid>
      <description>&lt;p&gt;&lt;em&gt;I tracked down the real reason the devlog had gone quiet for weeks, and ended up rebuilding the whole publishing pipeline&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the English version of a post originally written in Korean for my &lt;a href="https://finaltype.github.io/quant-blog/en/architecture/01_project_architecture/" rel="noopener noreferrer"&gt;algorithmic trading system devlog&lt;/a&gt;(new tab).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It had been quiet here for well over a week since the last devlog. Today I finally tracked down why, and ended up overhauling how this blog gets published in the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it had gone quiet
&lt;/h2&gt;

&lt;p&gt;The devlog is supposed to wrap itself up automatically every night. In practice, nothing had actually been produced since July 24th.&lt;/p&gt;

&lt;p&gt;Digging in, the automation itself had been running fine every night. The very last save step, though, kept getting stuck waiting on a permission approval that nobody was around to grant at that hour, so it quietly finished without leaving anything behind — for almost a month straight.&lt;/p&gt;

&lt;p&gt;The one thing that saved this from going unnoticed forever was that even a skipped day was supposed to leave a one-line log entry. I wrote up the full story separately in &lt;a href="https://finaltype.github.io/quant-blog/en/postmortems/2026-08-22_silent-automation-failure/" rel="noopener noreferrer"&gt;a postmortem&lt;/a&gt;(new tab).&lt;/p&gt;

&lt;p&gt;I fixed the root cause today, and while I was at it, made the automation sturdier — now, even on days with no pre-written draft, it investigates the actual work log directly and writes the post itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leaving Tistory
&lt;/h2&gt;

&lt;p&gt;This blog's publishing step had always been manual, since Tistory's write API was shut down a long time ago. Only drafting and format conversion were automated — actually posting meant pasting into the editor by hand.&lt;/p&gt;

&lt;p&gt;Today I decided to change that entirely. New posts now go out through GitHub Pages (a static site) and Dev.to (a developer community), while Tistory stays around only as an archive of past posts.&lt;/p&gt;

&lt;p&gt;I'd also considered another blogging platform along the way, but ruled it out once I confirmed its publishing API had recently moved behind a paywall. I settled on a combination that stays free.&lt;/p&gt;

&lt;p&gt;I spent today translating the entire existing backlog into English and moving it over to the new platforms. Going forward, daily devlogs, weekly summaries, and event-driven postmortems all run through the same pipeline — write in Korean first, then rebuild the site and publish to Dev.to in one pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  A new postmortem series
&lt;/h2&gt;

&lt;p&gt;Until now, incidents and bad calls only got a short paragraph inside a devlog. Starting today, anything serious enough to need real root-cause analysis gets pulled out into its own postmortem post.&lt;/p&gt;

&lt;p&gt;I published the first batch of that series today. Besides the automation gap above, they cover a hardware verdict that flipped within days and blind spots found in the verification tools themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  On the trading side — fully automating a GPU benchmark resume
&lt;/h2&gt;

&lt;p&gt;A benchmark that runs a local large model repeatedly to compare performance has been in progress, and it got interrupted yesterday while sharing GPU resources with another job.&lt;/p&gt;

&lt;p&gt;Today I wrote up a resume procedure so the benchmark can pick back up with a single command the moment the GPU frees up, without anyone watching it. I also added a safeguard that verifies the input data's fingerprint at startup, so a resume can't accidentally pick up different input.&lt;/p&gt;

&lt;p&gt;The same day, I also put together a survey of every module that uses the GPU, looking for places to cut back further. That's going to a separate AI advisor next week to help prioritize.&lt;/p&gt;




&lt;p&gt;Looking back, today was less about a single new feature and more about chasing down why things had gone quiet. Once I found the root cause, fixing it was quick — and along the way, I rebuilt this blog to run unattended for much longer stretches.&lt;/p&gt;

</description>
      <category>quant</category>
      <category>trading</category>
      <category>devlog</category>
    </item>
    <item>
      <title>완전자동매매 시스템에 사람이 직접 개입해야 했던 사례 3가지</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Sat, 22 Aug 2026 12:33:47 +0000</pubDate>
      <link>https://dev.to/finaltype/wanjeonjadongmaemae-siseuteme-sarami-jigjeob-gaeibhaeya-haessdeon-sarye-3gaji-21c6</link>
      <guid>https://dev.to/finaltype/wanjeonjadongmaemae-siseuteme-sarami-jigjeob-gaeibhaeya-haessdeon-sarye-3gaji-21c6</guid>
      <description>&lt;p&gt;&lt;em&gt;자동으로 돌아가게 만든 것과, 자동으로 끝까지 처리되는 것은 다른 문장이었습니다&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;이 시스템은 사람 승인 없이 스스로 판단하고 매매하는 걸 목표로 설계했습니다. &lt;a href="https://finaltype.github.io/quant-blog/architecture/05_execution_architecture/" rel="noopener noreferrer"&gt;실계좌 주문 실행과 안전장치&lt;/a&gt;(새 창)도 그 목표에 맞춰 만들었습니다.&lt;/p&gt;

&lt;p&gt;그런데 최근 한 달 사이 실계좌에서 세 번, 사람이 직접 개입해야 하는 상황이 있었습니다. 세 사례 모두 "왜 자동 로직이 이 상황을 못 넘겼는지"의 구조가 서로 달랐습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. 배분 규칙이 특정 주문을 구조적으로 굶겼다
&lt;/h2&gt;

&lt;p&gt;특정 종목 하나가 여러 날째 매도 계획이 서 있는데도 계속 팔리지 않는 걸 발견했습니다. 시스템은 매일 이 종목을 매도 후보로 올렸지만, 실제 주문까지는 못 갔습니다.&lt;/p&gt;

&lt;p&gt;원인은 하루 매매 한도를 여러 라운드에 나눠 배분하는 규칙이었습니다. 이 종목의 주문 금액이 그날 남은 매도 한도보다 항상 컸습니다. 라운드 순서를 아무리 바꿔도 통과할 수 없는 구조였습니다.&lt;/p&gt;

&lt;p&gt;한도 자체는 정상 작동하고 있었습니다. 문제는 "이번엔 못 나가도 다음 기회에 나간다"는 전제가 이 종목엔 애초에 성립하지 않았다는 점입니다. 잔여 한도가 매번 주문 금액보다 작으면, 기회는 계속 오지만 한 번도 충분하지 않습니다.&lt;/p&gt;

&lt;p&gt;당장 못 나간 주문 1건은 사람이 직접 처리했습니다. 실계좌에서 이뤄진 되돌릴 수 없는 매도였습니다. 이후 배분 규칙 자체를 손봐서 같은 구조로 다시 굶는 일이 없도록 정리했습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. 안전장치가 스냅샷과 누적치를 혼동했다
&lt;/h2&gt;

&lt;p&gt;다른 날엔 반대 방향의 사고가 있었습니다. 누적 손실을 감지하는 안전장치가 정상적인 매수 2건을 잘못 차단했습니다.&lt;/p&gt;

&lt;p&gt;지수는 그날 거의 보합이었는데, 이 안전장치가 재는 손실률은 훨씬 크게 찍혀 있었습니다. 원인을 보니 이 장치는 "고점 대비 누적 하락"을 감지하는 용도였는데, 정작 비교하는 현재값은 장중 순간 스냅샷이었습니다.&lt;/p&gt;

&lt;p&gt;장중 잠깐의 변동이 누적 지표를 밀어 올려서, 실제로는 발동하면 안 될 상황에서 발동한 겁니다. 누적을 재는 장치와 순간을 재는 장치가 뒤섞여 있었던 셈입니다.&lt;/p&gt;

&lt;p&gt;막힌 매수 2건은 사람이 판단해서 직접 집행했습니다. 이후 이 안전장치가 장중 순간값이 아니라 "그날 마감 대 전날 마감" 기준으로만 반응하도록 구조를 바꿨습니다. 장중 급락에는 이제 다른 안전장치가 대신 반응하도록 역할을 나눴습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. 개입 경로 자체가 "새로 사는 경우"를 몰랐다
&lt;/h2&gt;

&lt;p&gt;두 번째 사례를 수습하는 과정에서 사고가 하나 더 있었습니다. 수동으로 낸 주문을 원장에 반영하는 도구를 썼는데, 반영이 안 되고 조용히 빠졌습니다.&lt;/p&gt;

&lt;p&gt;이 도구는 사람이 손으로 낸 거래를 세 가지 경우 중 하나로 분류합니다. 기존 보유 종목을 판 경우, 기존 보유 종목을 더 산 경우, 그리고 시스템과 무관한 거래인 경우입니다.&lt;/p&gt;

&lt;p&gt;그런데 이번 매수는 원장에 없던 새 종목을 사람이 처음 사들인 경우였습니다. 세 분류 중 어디에도 안 맞았고, 도구는 이걸 "시스템과 무관한 거래"로 잘못 넘겼습니다. 그 결과 실제로는 산 자산이 잠깐 원장 밖에 있는 것처럼 표시됐습니다.&lt;/p&gt;

&lt;p&gt;이 도구는 애초에 사람 개입을 위해 만든 경로였습니다. 그런데 그 경로를 설계할 때, "사람이 아예 새로운 자리에 처음 진입하는 경우"는 상정하지 않았습니다. 개입 경로 자체가 개입의 한 형태를 놓치고 있었던 셈입니다.&lt;/p&gt;

&lt;p&gt;순서(먼저 다른 매도를 부기하고, 그다음 이 매수를 부기)를 지켜서 바로 수습했고, 검증 결과 원장과 실계좌 잔고는 정확히 일치했습니다. 분류 로직에 이 경우를 추가하는 건 아직 남은 과제입니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  세 사례를 묶어보면
&lt;/h2&gt;

&lt;p&gt;셋 다 "자동으로 처리되게 만들었다"와 "실제로 끝까지 처리된다"가 다른 문장이라는 걸 보여줬습니다.&lt;/p&gt;

&lt;p&gt;첫 번째는 규칙이 있었지만 그 규칙이 특정 입력에서 절대 통과할 수 없는 구조였습니다. 두 번째는 장치가 있었지만 재는 대상(순간 대 누적)이 설계 의도와 어긋나 있었습니다. 세 번째는 사람 개입을 위한 경로가 있었지만 그 경로 자체가 특정 개입 형태를 몰랐습니다.&lt;/p&gt;

&lt;p&gt;세 가지 모두 "자동화가 이 케이스를 놓칠 수 있다"는 걸 사전에 안 게 아니라, 실제로 놓친 뒤에야 알았습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  일반화하면
&lt;/h2&gt;

&lt;p&gt;완전자동을 목표로 설계할수록, 사람이 마지막에 손을 댈 수 있는 경로를 미리 만들어 둬야 한다는 걸 다시 확인했습니다. 자동화가 완벽해서가 아니라, 자동화가 어디서 뚫릴지 미리 다 알 수 없기 때문입니다.&lt;/p&gt;

&lt;p&gt;다만 그 개입 경로도 하나의 시스템 컴포넌트라서, 똑같이 설계 결함을 가질 수 있습니다. 세 번째 사례가 그걸 보여줬습니다. 사람이 개입하는 통로를 "예외 처리니까 대충 만들어도 된다"고 여기면, 그 통로 자체가 다음 사고 지점이 됩니다.&lt;/p&gt;

&lt;p&gt;그래서 개입 경로를 만들 때 세 가지를 같이 챙기는 게 나아 보입니다. 개입한 사실이 시스템에 자동으로 기록되게 할 것(사람이 기억에 의존하지 않도록), 개입이 가능한 상황의 종류를 미리 다 나열해볼 것(새로 사는 경우처럼 빠지기 쉬운 케이스가 있다).&lt;/p&gt;

&lt;p&gt;개입이 필요했던 이유(배분 규칙, 시간창 설계 등)는 그때그때 땜질하지 말고 근본 규칙을 고쳐서, 같은 개입이 반복되지 않게 하는 것도 함께 챙길 부분입니다.&lt;/p&gt;

&lt;p&gt;자동화의 목표는 사람을 완전히 빼는 게 아니라, 사람이 개입해야 할 때 그 개입이 안전하고 기록되게 만드는 것에 더 가까운 것 같습니다.&lt;/p&gt;

</description>
      <category>softwarequality</category>
      <category>postmortem</category>
      <category>debugging</category>
      <category>quant</category>
    </item>
    <item>
      <title>3 Cases Where Fully Automated Trading Still Needed a Human</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Sat, 22 Aug 2026 12:33:46 +0000</pubDate>
      <link>https://dev.to/finaltype/3-cases-where-fully-automated-trading-still-needed-a-human-5d0d</link>
      <guid>https://dev.to/finaltype/3-cases-where-fully-automated-trading-still-needed-a-human-5d0d</guid>
      <description>&lt;p&gt;&lt;em&gt;Making something run automatically and having it actually get handled to completion turned out to be two different sentences&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the English version of a post originally written in Korean for my &lt;a href="https://finaltype.github.io/quant-blog/en/architecture/01_project_architecture/" rel="noopener noreferrer"&gt;algorithmic trading system devlog&lt;/a&gt;(new tab).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I designed this system to judge and trade on its own, without needing human approval for each decision. The &lt;a href="https://finaltype.github.io/quant-blog/en/architecture/05_execution_architecture/" rel="noopener noreferrer"&gt;order execution and safety-guard layer&lt;/a&gt;(new tab) was built around that same goal.&lt;/p&gt;

&lt;p&gt;Over the past month, though, there were three separate moments where I had to step in and act directly on the live account. Each one failed for a structurally different reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. An allocation rule structurally starved one order
&lt;/h2&gt;

&lt;p&gt;I noticed a particular ticker had a sell plan queued for several days running, yet it never actually went out. The system kept nominating it as a sell candidate every day, but the order never reached execution.&lt;/p&gt;

&lt;p&gt;The cause was the rule that splits the daily trading budget across multiple rounds. This position's order size was consistently larger than whatever sell budget remained that day. No matter how the rounds were reordered, it could never clear.&lt;/p&gt;

&lt;p&gt;The budget cap itself was working exactly as designed. The problem was that the underlying assumption — "if it doesn't clear this time, it'll clear next time" — never held for this position. New opportunities kept arriving, but none of them was ever big enough.&lt;/p&gt;

&lt;p&gt;I executed the one blocked order by hand. It was an irreversible sell on the live account. Afterward, I reworked the allocation rule itself so the same starvation pattern couldn't recur.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A safety guard confused a snapshot with a cumulative reading
&lt;/h2&gt;

&lt;p&gt;On a different day, the opposite kind of failure happened. A guard meant to detect cumulative drawdown wrongly blocked two legitimate buy orders.&lt;/p&gt;

&lt;p&gt;The index was nearly flat that day, but the loss figure this guard was tracking read much larger. Looking closer, the guard was designed to measure "decline from peak," but the current value it compared against was an intraday snapshot.&lt;/p&gt;

&lt;p&gt;A brief intraday swing pushed the cumulative metric up, tripping the guard in a situation where it shouldn't have fired at all. A gauge meant to track cumulative state and a gauge meant to read an instant were tangled together.&lt;/p&gt;

&lt;p&gt;I judged the two blocked buys were legitimate and executed them by hand. I then changed the guard to only react to "today's close versus yesterday's close," never an intraday reading. A separate, faster guard now handles sharp intraday drops instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The intervention path itself didn't know about "buying something new"
&lt;/h2&gt;

&lt;p&gt;While cleaning up after the second case, a third incident happened. I used the tool built to record manually-placed orders into the ledger, and the entry silently failed to land.&lt;/p&gt;

&lt;p&gt;That tool classifies any hand-placed trade into one of three categories: selling an existing position, adding to an existing position, or a trade unrelated to the system.&lt;/p&gt;

&lt;p&gt;But this particular buy was a brand-new position — something not in the ledger before. It didn't fit any of the three categories, so the tool defaulted it to "unrelated to the system." As a result, an asset I had actually bought briefly showed up as if it sat outside the ledger entirely.&lt;/p&gt;

&lt;p&gt;This tool existed specifically to handle human intervention. But when it was designed, nobody accounted for the case of a human opening a brand-new position from scratch. The intervention path itself was missing one form of intervention.&lt;/p&gt;

&lt;p&gt;I fixed it by following the correct sequence — booking the other day's sells first, then this buy — and verification confirmed the ledger matched the live account exactly. Adding this missing case to the classification logic is still on the to-do list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting the three together
&lt;/h2&gt;

&lt;p&gt;All three showed the same gap: "built to run automatically" and "actually carried through to completion" are two different sentences.&lt;/p&gt;

&lt;p&gt;The first had a rule in place, but that rule was structurally impossible to clear for a particular input. The second had a guard in place, but what it measured — instant versus cumulative — didn't match what it was meant to measure. The third had a human-intervention path in place, but that path itself didn't know about one form of intervention.&lt;/p&gt;

&lt;p&gt;In all three cases, I didn't know in advance that automation could miss this specific case. I only found out after it already had.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generalizing
&lt;/h2&gt;

&lt;p&gt;This reconfirmed something for me: the more fully automated a system is meant to be, the more important it is to build in a path for a human to step in at the last moment. Not because automation is imperfect in some abstract sense, but because you can't fully know in advance where it will break.&lt;/p&gt;

&lt;p&gt;But that intervention path is itself a system component, and it can carry the same kind of design flaw as anything else. The third case made that clear. Treating a human-intervention path as "just an exception handler, doesn't need much design" turns that path into the next failure point.&lt;/p&gt;

&lt;p&gt;So when I build an intervention path now, I try to keep a few things together. Make the fact that a human intervened get recorded automatically, rather than depending on memory, and enumerate the situations where intervention might be needed in advance — cases like "opening a brand-new position" are easy to miss.&lt;/p&gt;

&lt;p&gt;When intervention was needed because of some underlying rule — an allocation rule, a time-window design — fixing that root rule instead of patching around it each time matters just as much, so the same intervention doesn't keep recurring.&lt;/p&gt;

&lt;p&gt;The goal of automation isn't removing the human entirely. It's closer to making sure that when a human does have to step in, that intervention is safe and gets recorded.&lt;/p&gt;

</description>
      <category>softwarequality</category>
      <category>postmortem</category>
      <category>debugging</category>
      <category>quant</category>
    </item>
    <item>
      <title>개발일지 자동화가 한 달 가까이 멈춰 있었던 이유</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Sat, 22 Aug 2026 12:28:40 +0000</pubDate>
      <link>https://dev.to/finaltype/gaebalilji-jadonghwaga-han-dal-gaggai-meomcweo-isseossdeon-iyu-e57</link>
      <guid>https://dev.to/finaltype/gaebalilji-jadonghwaga-han-dal-gaggai-meomcweo-isseossdeon-iyu-e57</guid>
      <description>&lt;p&gt;&lt;em&gt;로그조차 안 쌓이니, 돌고 있는지 죽어 있는지 구분할 방법이 없었습니다&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;이 블로그의 개발일지는 매일 밤 자동으로 마무리됩니다. 그날 대화로 초안을 썼으면 변환해서 로그에 남기고, 없으면 스킵했다는 한 줄만 남깁니다. 최근 이 파이프라인을 들여다볼 일이 있었는데, 7월 24일 이후로 로그가 통째로 비어 있었습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  무슨 일이 있었나
&lt;/h2&gt;

&lt;p&gt;자동화 로그(&lt;code&gt;.automation.log&lt;/code&gt;) 마지막 줄이 2026-07-24였습니다. 그 뒤로 8월 22일까지, 거의 한 달 가까이 스킵 기록조차 한 줄도 없었습니다.&lt;/p&gt;

&lt;p&gt;자동화가 아예 안 돌았나 싶어서 실행 로그(&lt;code&gt;cron_output.log&lt;/code&gt;)를 열어봤습니다. 그런데 거기엔 매일 밤 실행된 흔적이 빼곡했습니다.&lt;/p&gt;

&lt;p&gt;날짜 확인하고, 초안 있으면 내용 정리하고, 크로스링크까지 챙긴 요약이 매일 밤 남아 있었습니다. 일은 하고 있었는데, 결과물만 하나도 남지 않고 있었던 겁니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  왜 아무도 몰랐나
&lt;/h2&gt;

&lt;p&gt;실행 로그를 읽어보니 원인은 매일 같았습니다. 파일 쓰기 권한 승인을 기다리다가 그대로 끝난 겁니다.&lt;/p&gt;

&lt;p&gt;"workspace has not been trusted"라는 경고가 매 실행마다 찍혀 있었습니다. 초안을 잘 정리해놓고도, 마지막 파일 쓰기 한 줄이 승인 대기에 막혀서 아무것도 저장되지 않은 채 세션이 끝나는 패턴이 한 달 가까이 반복됐습니다.&lt;/p&gt;

&lt;p&gt;문제는 이게 하필 &lt;strong&gt;로그를 남기는 단계 자체가 막힌 상황&lt;/strong&gt;이었다는 겁니다. 스킵한 날엔 스킵했다는 한 줄도 못 남겼습니다. 그러니 로그만 보면 "자동화가 멈췄다"와 "쓸 게 없어서 조용했다"를 구분할 수가 없었습니다.&lt;/p&gt;

&lt;p&gt;무인 자동화이니 매일 밤 누가 화면을 지켜보는 것도 아닙니다. 결과적으로 이 공백은 사람이 우연히 로그 파일을 열어보기 전까지는 발견될 방법이 없는 구조였습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  진짜 원인
&lt;/h2&gt;

&lt;p&gt;원인은 신뢰(trust) 설정이었습니다. 이 헤드리스 자동화 세션은 프로젝트 폴더 단위로 파일 쓰기 권한을 신뢰받아야 동작하는데, 그 신뢰 설정 키가 이 블로그 폴더가 아니라 상위 디렉터리(프로젝트들이 모여 있는 루트) 단위로 걸려 있었습니다.&lt;/p&gt;

&lt;p&gt;즉 이 블로그 폴더만 놓고 보면 "아직 한 번도 대화형으로 신뢰 승인을 받은 적 없는 새 작업공간" 취급을 받고 있었던 셈입니다. 설정 파일 안에 이미 허용 규칙 10개가 들어 있었는데도, 그 규칙들이 걸려 있는 범위 자체가 무시되고 있었습니다.&lt;/p&gt;

&lt;p&gt;헤드리스로 도는 야간 자동화는 대화형 승인 프롬프트에 응답할 사람이 없습니다. 범위가 어긋난 신뢰 설정 하나가, 매일 밤 정확히 같은 지점에서 조용히 실행을 무력화하고 있었던 겁니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  흥미로운 디테일 하나 — 유령 완료 기록
&lt;/h2&gt;

&lt;p&gt;이 공백을 되짚어보다가 특이한 줄 하나를 발견했습니다. 8월 22일 낮 12시 41분에 "weekly: 완료"라는 로그 한 줄이 남아 있었는데, 그 시각에 대응하는 실제 산출물 파일은 없었습니다.&lt;/p&gt;

&lt;p&gt;같은 날 오후 4시에 다시 수동으로 실행된 기록이 있었고, 이번엔 실제 파일까지 정상적으로 만들어졌습니다. 앞선 12시 41분 기록이 왜 실물 없이 "완료"라고만 남았는지는 원인을 특정하지 못했습니다.&lt;/p&gt;

&lt;p&gt;권한 문제가 한창이던 구간이라 어떤 형태로든 쓰기 절차 일부만 성공하고 일부는 실패한 걸로 추정만 할 뿐입니다. 다만 이 한 줄은 별도로 눈에 띄는 교훈을 남겼습니다. &lt;strong&gt;"완료"라고 적힌 로그도 그 자체로 완전히 믿을 수는 없다&lt;/strong&gt;는 겁니다. 로그와 실제 산출물을 따로 대조하지 않았다면 이 유령 기록을 그냥 지나쳤을 겁니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  어떻게 고쳤나
&lt;/h2&gt;

&lt;p&gt;신뢰 설정을 이 블로그 폴더 기준으로 다시 걸어주니, 그날 밤부터 바로 정상화됐습니다. 별도의 복잡한 조치는 필요 없었습니다.&lt;/p&gt;

&lt;p&gt;문제는 고치는 방법이 아니라, 한 달 가까이 그 문제를 놓치고 있었다는 사실 쪽이었습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  일반화된 교훈
&lt;/h2&gt;

&lt;p&gt;이번 일로 다시 확인한 건, 무인 자동화에서 "로그가 없다"는 상태 자체가 하나의 신호라는 겁니다. 그런데 그 신호를 신호로 취급하려면, 애초에 "침묵"과 "성공적인 무동작"을 구분할 수 있게 설계돼 있어야 합니다.&lt;/p&gt;

&lt;p&gt;이번 파이프라인은 스킵한 날에도 로그 한 줄을 남기게 되어 있었습니다. 그 설계 덕분에, "로그가 아예 안 쌓이고 있다"는 것 자체가 정상 범위를 벗어난 상태라는 걸 뒤늦게라도 알아챌 수 있었습니다.&lt;/p&gt;

&lt;p&gt;만약 스킵 로그라는 장치가 없었다면, "초안이 계속 없는 조용한 한 달이었나 보다"로 착각하고 넘어갔을 수도 있습니다. CI/CD 파이프라인이든 cron 잡이든 백그라운드로 도는 배치 작업이든, 결과가 안 나온다고 바로 알림이 울리는 구조가 아니라면 이런 공백은 똑같이 생길 수 있습니다.&lt;/p&gt;

&lt;p&gt;무인으로 도는 무언가를 설계할 때는, "잘 돌고 있어서 조용한 것"과 "안 돌고 있어서 조용한 것"을 사람이 구분할 수 있는 장치를 반드시 하나는 심어둬야 한다는 걸 다시 배웠습니다. 하트비트든 스킵 로그든, 뭐든 상관없습니다. 아무 신호도 없는 상태를 정상으로 오인하지만 않으면 됩니다.&lt;/p&gt;

</description>
      <category>softwarequality</category>
      <category>postmortem</category>
      <category>debugging</category>
      <category>quant</category>
    </item>
    <item>
      <title>Why the Devlog Automation Sat Broken for Nearly a Month</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Sat, 22 Aug 2026 12:28:38 +0000</pubDate>
      <link>https://dev.to/finaltype/why-the-devlog-automation-sat-broken-for-nearly-a-month-23fm</link>
      <guid>https://dev.to/finaltype/why-the-devlog-automation-sat-broken-for-nearly-a-month-23fm</guid>
      <description>&lt;p&gt;&lt;em&gt;With no logs at all, there was no way to tell "running" apart from "dead"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the English version of a post originally written in Korean for my &lt;a href="https://finaltype.github.io/quant-blog/en/architecture/01_project_architecture/" rel="noopener noreferrer"&gt;algorithmic trading system devlog&lt;/a&gt;(new tab).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This blog's devlog gets wrapped up automatically every night. If I'd written a draft in conversation that day, it gets converted and logged; if not, one skip line gets logged instead. I recently had reason to look at this pipeline, and found the log had been completely empty since July 24th.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;The last line in the automation log (&lt;code&gt;.automation.log&lt;/code&gt;) was dated 2026-07-24. After that, all the way through August 22nd — nearly a month — not even a single skip line had been recorded.&lt;/p&gt;

&lt;p&gt;I wondered if the automation had simply stopped running, so I opened the execution log (&lt;code&gt;cron_output.log&lt;/code&gt;). It was packed with traces of runs, every single night.&lt;/p&gt;

&lt;p&gt;Checking the date, summarizing the draft when one existed, wiring up cross-links — a full night's worth of work was recorded there, every night. The work was happening. Nothing was just failing to be saved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nobody noticed
&lt;/h2&gt;

&lt;p&gt;Reading through the execution log, the cause was the same every night: the run got stuck waiting for a file-write permission approval that never came.&lt;/p&gt;

&lt;p&gt;A "workspace has not been trusted" warning was printed on every single run. The draft would be neatly prepared, and then the very last step — writing the file — would sit blocked on approval until the session simply ended. This pattern repeated for nearly a month.&lt;/p&gt;

&lt;p&gt;The problem was that this happened to block the exact step that would have left a log trail. On skip days, not even the skip line could be written. So the log itself couldn't distinguish "the automation stopped" from "there was quietly nothing to write."&lt;/p&gt;

&lt;p&gt;Since this is unattended automation, nobody is watching a screen every night either. As a result, this gap had no way of surfacing until someone happened to open the log file by chance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real cause
&lt;/h2&gt;

&lt;p&gt;The cause was a trust setting. This headless automation session needs its working folder to be trusted before it can write files, but the key for that trust setting was scoped one level too high — to the parent directory that holds all the projects, not to this blog's own folder.&lt;/p&gt;

&lt;p&gt;Looked at from this blog folder alone, it was being treated as "a brand-new workspace that has never gone through interactive trust approval." Ten allow-rules already existed in the settings file, but the scope those rules were keyed to was being ignored entirely.&lt;/p&gt;

&lt;p&gt;Unattended overnight automation has nobody around to answer an interactive approval prompt. One trust setting scoped to the wrong level was quietly disarming the run at exactly the same point, every single night.&lt;/p&gt;

&lt;h2&gt;
  
  
  One interesting detail — a ghost completion record
&lt;/h2&gt;

&lt;p&gt;While retracing this gap, I found one unusual line. At 12:41 PM on August 22nd, a "weekly: complete" line had been logged, but no actual output file existed for that timestamp.&lt;/p&gt;

&lt;p&gt;Later that same day, at 4 PM, there was a separate manual run recorded, and this one did produce the actual file correctly. What caused the earlier 12:41 PM entry to log "complete" with nothing actually written couldn't be pinned down.&lt;/p&gt;

&lt;p&gt;It landed in the middle of the permission trouble, so the best guess is that some part of the write sequence succeeded while another part failed. Either way, this one line carried its own separate lesson: &lt;strong&gt;a log line that says "complete" isn't fully trustworthy on its own.&lt;/strong&gt; Without cross-checking the log against the actual output file, this ghost entry would have gone unnoticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was fixed
&lt;/h2&gt;

&lt;p&gt;Rescoping the trust setting to this blog folder specifically fixed it immediately, starting that same night. No complicated remediation was needed.&lt;/p&gt;

&lt;p&gt;The real problem wasn't how to fix it. It was that the problem had gone unnoticed for nearly a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general lesson
&lt;/h2&gt;

&lt;p&gt;What this confirmed, once again, is that "no logs" is itself a signal in unattended automation. But treating that signal as a signal requires the system to be designed, from the start, to distinguish silence from successful inaction.&lt;/p&gt;

&lt;p&gt;This pipeline was built to log one line even on skip days. That design is exactly what made it possible to eventually notice that "logs stopped accumulating entirely" was itself out of normal range.&lt;/p&gt;

&lt;p&gt;If that skip-log mechanism hadn't existed, it would have been easy to mistake the gap for "must have just been a quiet month with no drafts." Whether it's a CI/CD pipeline, a cron job, or any background batch process, this same gap can appear whenever the absence of output doesn't itself trigger an alert.&lt;/p&gt;

&lt;p&gt;I relearned that when designing anything meant to run unattended, you need at least one mechanism that lets a person tell apart "quiet because it's working fine" from "quiet because it's dead." A heartbeat, a skip log — the specific mechanism doesn't matter. What matters is never letting a total absence of signal get mistaken for normal.&lt;/p&gt;

</description>
      <category>softwarequality</category>
      <category>postmortem</category>
      <category>debugging</category>
      <category>quant</category>
    </item>
    <item>
      <title>자동매매 안전장치 오작동 - 판정 기준의 시간 단위가 잘못됐다</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Sat, 22 Aug 2026 12:23:32 +0000</pubDate>
      <link>https://dev.to/finaltype/jadongmaemae-anjeonjangci-ojagdong-panjeong-gijunyi-sigan-danwiga-jalmosdwaessda-4nn3</link>
      <guid>https://dev.to/finaltype/jadongmaemae-anjeonjangci-ojagdong-panjeong-gijunyi-sigan-danwiga-jalmosdwaessda-4nn3</guid>
      <description>&lt;p&gt;&lt;em&gt;누적 손실을 막으려던 안전장치가 하루 안의 일시적 하락에도 반응했습니다 — 두 지표를 같은 시간 단위로 재고 있는지가 핵심이었습니다&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;자동매매 시스템에는 손실이 누적될 때 신규 매수를 막는 안전장치가 있습니다. 최근 이 장치가 정상적인 매수 2건을 잘못 막는 일이 있었습니다.&lt;/p&gt;

&lt;p&gt;원인을 따라가 보니 코드에 버그가 있었던 게 아니라, 판정에 쓰는 두 숫자가 서로 다른 시간 단위로 재고 있었습니다. 그 경위를 정리해봤습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  무슨 일이 있었나
&lt;/h2&gt;

&lt;p&gt;어느 날 오전, &lt;a href="https://finaltype.github.io/quant-blog/architecture/05_execution_architecture/" rel="noopener noreferrer"&gt;실계좌 주문 실행/안전장치 계층&lt;/a&gt;(새 창) 중 하나가 정상적인 매수 신호 2건을 차단했습니다.&lt;/p&gt;

&lt;p&gt;당일 지수는 사실상 보합에 가까웠습니다. 그런데 이 계좌의 장중 평가금액만 유독 크게 빠져 있었고, 그 순간값을 근거로 "누적 손실이 위험 수위"라는 판정이 내려졌습니다.&lt;/p&gt;

&lt;p&gt;더 이상한 건 당일 급락을 감시하는 다른 안전장치(서킷브레이커 층)는 전혀 반응하지 않고 있었다는 점입니다. 급락 감시 장치는 조용한데, 누적 손실 감시 장치만 발화한 상황이었습니다.&lt;/p&gt;

&lt;p&gt;일단 그 자리에서는 사람이 직접 판단해 차단된 매수를 수동으로 집행했습니다. 시스템 자체는 오작동이 아니라 설계된 대로 판정했다는 걸 먼저 확인한 뒤였습니다. 문제는 그 판정 로직 안에 있었습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  안전장치가 왜 그렇게 설계돼 있었나
&lt;/h2&gt;

&lt;p&gt;이 장치는 계좌가 고점 대비 얼마나 밀렸는지, 즉 누적 낙폭(peak-to-trough)을 감시합니다.&lt;/p&gt;

&lt;p&gt;전략이 며칠에 걸쳐 서서히 열화되는 경우를 잡기 위한 장치입니다. 하루 급락은 다른 층(서킷브레이커)이 이미 담당하고 있으니, 이 장치는 "여러 날에 걸친 완만한 침식"을 보는 게 원래 역할이었습니다.&lt;/p&gt;

&lt;p&gt;그런데 판정에 쓰는 두 숫자 중 하나는 마감 기준 시계열에서 뽑은 고점이었고, 다른 하나는 판정을 호출하는 시점의 장중 평가금액이었습니다.&lt;/p&gt;

&lt;p&gt;설계 당시엔 "가장 최신 정보를 쓰는 게 더 안전하다"는 생각으로 장중값을 그대로 넘겼을 겁니다. 실시간성을 포기하지 않으려는 선택이었던 셈입니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  진짜 원인 — 기준선과 현재값의 시간 단위가 달랐다
&lt;/h2&gt;

&lt;p&gt;고점은 하루 단위(마감가)로 갱신되는데, 그 고점과 비교하는 현재값은 분 단위(장중 스냅샷)였습니다.&lt;/p&gt;

&lt;p&gt;누적 지표인데 비교 대상 하나가 순간값이면, 하루 안의 정상적인 변동성만으로도 "누적 손실이 위험 수위"라는 판정이 나올 수 있습니다. 실제로 그 사고 당일이 정확히 그런 경우였습니다 — 지수는 거의 안 움직였는데 장중 한 시점의 평가금액만 일시적으로 빠져 있었고, 그 시점에 하필 판정이 걸렸습니다.&lt;/p&gt;

&lt;p&gt;과거 구간을 마감 대 마감 기준으로 다시 계산해보니, 같은 기간 동안 이 임계값을 넘긴 적이 실제로는 없었습니다. 즉 이 장치가 실제로 "위험한 누적 손실"을 잡은 게 아니라, 시간 단위가 안 맞는 비교 때문에 존재하지 않던 손실을 본 것이었습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  어떻게 고쳤나
&lt;/h2&gt;

&lt;p&gt;가장 먼저 한 일은 판정 기준을 마감 대 마감으로 바꾼 것입니다. 고점도 마감 시계열, 현재값도 마감 시계열 — 두 숫자를 같은 시간 단위로 맞췄습니다.&lt;/p&gt;

&lt;p&gt;그런데 이렇게 하면 다른 문제가 생깁니다. 만약 정말로 위험한 급락이 장중에 벌어지면, 이 장치는 그날 마감까지 아무 반응을 하지 않게 됩니다. 완만한 침식을 잡으려다가 급격한 위험까지 하루 늦게 잡는 쪽으로 옮겨간 셈입니다.&lt;/p&gt;

&lt;p&gt;그래서 임계값을 둘로 나눠 다르게 다루기로 했습니다. 상대적으로 자주 걸리고 놓쳐도 다음 마감에 다시 잡히는 완만한 쪽은 마감 대 마감 기준을 유지하고, 드물지만 놓치면 하루 더 손실이 방치되는 급격한 쪽만 장중 기준으로 되돌렸습니다. 두 임계값이 감시하는 위험의 성격 자체가 달랐던 겁니다.&lt;/p&gt;

&lt;p&gt;이 과정에서 부수적으로 하나 더 발견했습니다. 하루 뒤 아침에 자동으로 도는 리셋 로직이, 급격한 위험 판정보다 먼저 돌면서 판정 대상이 되어야 할 낙폭을 조용히 지워버리는 경로가 있었습니다. 판정 순서를 뒤집어 리셋보다 위험 판정이 먼저 돌도록 고쳤습니다. 안전장치를 고치다가 그 안전장치를 무력화하는 또 다른 경로를 발견한 경우입니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  일반화된 교훈
&lt;/h2&gt;

&lt;p&gt;이번 사고에서 가장 크게 배운 건 "안전장치의 판정 기준이 무엇을 감시하려는 지표인지"와 "실제로 비교에 쓰는 값이 어떤 시간 단위인지"를 항상 함께 확인해야 한다는 점입니다.&lt;/p&gt;

&lt;p&gt;누적/추세 지표는 원래 여러 시점에 걸친 완만한 변화를 감시하려고 만든 것입니다. 거기에 순간값 하나를 섞으면, 그 순간의 정상적인 변동성이 누적 지표의 판정을 흔들어버립니다. 지표의 이름이 "누적"이어도 구현이 실제로 그 이름값을 하고 있는지는 별개의 질문이었습니다.&lt;/p&gt;

&lt;p&gt;반대 방향의 함정도 있습니다. 시간 단위를 무조건 느슨하게(예: 전부 마감 기준으로) 통일하면 이번엔 진짜 급격한 위험까지 놓칠 수 있습니다. 이번 사고에서도 안전장치를 고치다가 "급격한 위험은 여전히 즉각 반응해야 한다"는 요구를 뒤늦게 떠올렸습니다.&lt;/p&gt;

&lt;p&gt;결국 정답은 위험의 성격별로 판정 시간창을 다르게 가져가는 것이었습니다. 자주 오탐하지만 놓쳐도 회복 가능한 쪽은 느슨한 시간창으로, 드물지만 놓치면 치명적인 쪽은 촘촘한 시간창으로. 하나의 안전장치 안에 서로 다른 위험을 하나의 시간 단위로 뭉뚱그려 넣지 않는 것 — 이게 이번 사고가 남긴 가장 일반적인 교훈이었습니다.&lt;/p&gt;

&lt;p&gt;안전장치를 하나 손볼 때마다, 그 옆에 있는 다른 안전장치와의 경계가 어디인지, 그리고 위험이 어느 쪽으로 옮겨가는지를 같이 적어두려고 합니다. 커버리지가 사라지는 게 아니라 관할이 옮겨가는 것일 뿐이라면, 그걸 기록해두지 않으면 다음에는 그게 버그로 보일 겁니다.&lt;/p&gt;

</description>
      <category>softwarequality</category>
      <category>postmortem</category>
      <category>debugging</category>
      <category>quant</category>
    </item>
    <item>
      <title>A Trading Safety Guard Misfired - The Time Scale of the Threshold Was Wrong</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Sat, 22 Aug 2026 12:23:31 +0000</pubDate>
      <link>https://dev.to/finaltype/a-trading-safety-guard-misfired-the-time-scale-of-the-threshold-was-wrong-iej</link>
      <guid>https://dev.to/finaltype/a-trading-safety-guard-misfired-the-time-scale-of-the-threshold-was-wrong-iej</guid>
      <description>&lt;p&gt;&lt;em&gt;A guard meant to catch cumulative losses reacted to a brief intraday dip instead — the fix came down to whether two numbers were measured on the same time scale&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the English version of a post originally written in Korean for my &lt;a href="https://finaltype.github.io/quant-blog/architecture/01_project_architecture/" rel="noopener noreferrer"&gt;algorithmic trading system devlog&lt;/a&gt;(new tab).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My automated trading system has a safety guard that blocks new buys once losses accumulate past a certain point. Recently, that guard wrongly blocked two perfectly normal buy orders.&lt;/p&gt;

&lt;p&gt;Tracing it back, the problem wasn't a bug in the usual sense. Two numbers feeding into the same judgment were measured on different time scales. Here's how it played out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;One morning, one of the &lt;a href="https://finaltype.github.io/quant-blog/en/architecture/05_execution_architecture/" rel="noopener noreferrer"&gt;execution and safety-guard layers&lt;/a&gt;(new tab) that sit between a signal and a live order blocked two normal buy signals.&lt;/p&gt;

&lt;p&gt;The market index that day was essentially flat. Yet this account's intraday valuation had dropped noticeably, and the guard fired a judgment of "cumulative loss at a dangerous level" based on that one instantaneous reading.&lt;/p&gt;

&lt;p&gt;What made it stranger: the guard that watches for a same-day crash (a separate circuit-breaker layer) hadn't reacted at all. The crash monitor stayed quiet while the cumulative-loss monitor fired.&lt;/p&gt;

&lt;p&gt;In the moment, I stepped in and manually placed the blocked buys myself, after confirming the system wasn't malfunctioning — it was judging exactly as designed. The real problem was inside that design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the guard was built that way
&lt;/h2&gt;

&lt;p&gt;This particular guard watches how far the account has fallen from its recent peak — a peak-to-trough drawdown check.&lt;/p&gt;

&lt;p&gt;It exists to catch a strategy slowly deteriorating over several days. A same-day crash is already someone else's job (the circuit-breaker layer), so this guard's whole reason for existing was to see "slow erosion across multiple days."&lt;/p&gt;

&lt;p&gt;But of the two numbers it compared, one came from an end-of-day time series, and the other was whatever intraday valuation happened to be passed in at the moment the check ran.&lt;/p&gt;

&lt;p&gt;At design time, the reasoning was probably "using the freshest number available is safer." It was a deliberate choice not to give up real-time responsiveness.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real cause — the baseline and the current value used different time scales
&lt;/h2&gt;

&lt;p&gt;The peak updates once a day (end-of-day close). The value compared against that peak was a minute-by-minute intraday snapshot.&lt;/p&gt;

&lt;p&gt;Mix a cumulative metric with one instantaneous input, and ordinary intraday noise alone can trigger a "cumulative loss at a dangerous level" verdict. That's exactly what happened — the index barely moved, but the account's valuation happened to dip at the one moment the check ran, and that moment is when the judgment fired.&lt;/p&gt;

&lt;p&gt;Recomputing the same historical window on a close-to-close basis, the threshold had actually never been crossed in that period. The guard hadn't been catching a real cumulative loss at all — it had been seeing a loss that only existed because of the time-scale mismatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I fixed it
&lt;/h2&gt;

&lt;p&gt;The first fix was straightforward: switch the comparison to close-to-close. Peak from the daily series, current value from the daily series too — both numbers on the same time scale.&lt;/p&gt;

&lt;p&gt;But that creates a different problem. If a genuinely dangerous crash happens intraday, this guard now stays silent until the next close. Fixing the slow-erosion case pushed the fast-danger case a day behind.&lt;/p&gt;

&lt;p&gt;So I split the two thresholds and treated them differently. The threshold that fires relatively often but is forgiving to miss — because the next close catches it anyway — stayed on close-to-close. The threshold that fires rarely but, if missed, leaves the account trading through a further day of collapse, went back to intraday evaluation. The two thresholds were guarding against risks of a genuinely different character.&lt;/p&gt;

&lt;p&gt;Along the way, I found a second, related issue. A reset routine that runs automatically the next morning could run before the fast-danger judgment did, quietly erasing the drawdown that judgment was supposed to see. I reordered it so the danger check runs before the reset. Fixing one guard surfaced a second path that had been quietly disarming it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The generalized lesson
&lt;/h2&gt;

&lt;p&gt;The biggest takeaway was that "what a guard's threshold is meant to watch for" and "what time scale the value actually compared against it uses" have to be checked together, every time.&lt;/p&gt;

&lt;p&gt;A cumulative or trend metric exists specifically to catch gradual change across multiple points in time. Mix in a single instantaneous value, and that moment's ordinary noise can hijack the cumulative judgment. A metric being named "cumulative" doesn't guarantee the implementation actually behaves cumulatively — that turned out to be a separate question worth checking explicitly.&lt;/p&gt;

&lt;p&gt;There's a trap in the opposite direction too. Loosen every time scale uniformly (e.g., force everything onto end-of-day) and you'll miss a genuinely fast-moving danger instead. I hit exactly that while fixing this guard — I had to remember partway through that some risks still need to be caught immediately.&lt;/p&gt;

&lt;p&gt;The real answer was to let the judgment window differ by the character of the risk. Give a loosely-timed window to the kind of threshold that fires often but is cheap to miss, and a tightly-timed window to the kind that fires rarely but is expensive to miss. Don't collapse two different risks into one guard measured on one time scale — that's the most general lesson this incident left behind.&lt;/p&gt;

&lt;p&gt;Every time I touch one safety guard now, I try to also write down where its boundary with the neighboring guard sits, and which direction responsibility shifts to when I narrow this one's scope. If coverage isn't disappearing but just moving somewhere else, and I don't write that down, the next person reads it as a bug.&lt;/p&gt;

</description>
      <category>softwarequality</category>
      <category>postmortem</category>
      <category>debugging</category>
      <category>quant</category>
    </item>
    <item>
      <title>그래픽카드 교체 성능 비교 - 판정이 두 번 뒤집힌 이유</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Sat, 22 Aug 2026 12:18:24 +0000</pubDate>
      <link>https://dev.to/finaltype/geuraepigkadeu-gyoce-seongneung-bigyo-panjeongi-du-beon-dwijibhin-iyu-9kf</link>
      <guid>https://dev.to/finaltype/geuraepigkadeu-gyoce-seongneung-bigyo-panjeongi-du-beon-dwijibhin-iyu-9kf</guid>
      <description>&lt;p&gt;&lt;em&gt;처음 방향 판정도, 나중 추가구매 판정도 서로 다른 이유로 틀렸습니다 — 벤치마크를 읽는 방식 자체가 문제였습니다&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;그래픽카드를 다른 브랜드 제품으로 교체하면서, 새 카드에 맞는 실행 방식을 골라야 했습니다.&lt;/p&gt;

&lt;p&gt;후보가 두 갈래였는데, 처음엔 그중 하나가 확실히 낫다고 판단했습니다. 그런데 이 판단이 방향 자체가 반대였다는 게 곧 드러났고, 그 뒤로도 판정이 한 번 더 뒤집혔습니다. 정리하면서 왜 이런 일이 반복됐는지 짚어봤습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  첫 판정 — 방향이 반대였다
&lt;/h2&gt;

&lt;p&gt;새 카드로 넘어오면서 두 실행 방식 중 하나를 골라야 했습니다. 참고할 만한 외부 자료를 찾아 비교했더니, 지금 쓰는 속도 향상 기법(다음 토큰을 미리 여러 개 예측해두는 방식)이 한쪽 방식에서는 잘 안 먹힌다는 결론이 나왔습니다.&lt;/p&gt;

&lt;p&gt;이 결론대로면 그 기법을 포기하는 게 맞았습니다. 그런데 다음 날 다시 들여다보니 정반대였습니다. 실제로는 그 기법이 아예 지원되지 않는 게 아니라, 정확성 자체가 깨지는(출력이 쓰레기값으로 나오는) 문제였습니다.&lt;/p&gt;

&lt;p&gt;원인은 참고했던 성능 수치를 잘못 읽은 데 있었습니다. 인용한 두 숫자 중 하나가 지금 쓰는 것과 다른 종류의 모델(계산량이 훨씬 적은 구조) 결과였는데, 이름이 비슷해서 같은 모델로 착각했습니다. 어떤 계산으로 역산해보니 그 숫자는 지금 쓰는 모델 구조로는 물리적으로 나올 수 없는 값이었고, 그제야 오독이 드러났습니다.&lt;/p&gt;

&lt;p&gt;방향을 바로잡고 나서 실제로 배포까지 갔습니다. 카드 한 장으로 예전 두 장과 비슷하거나 더 나은 처리 속도가 나왔고, 배치 크기 같은 세부 설정도 함께 튜닝해서 실측으로 확인했습니다. 이 단계까지는 판정이 안정된 것처럼 보였습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  두 번째 판정 — 추가 구매를 검토하다 또 뒤집혔다
&lt;/h2&gt;

&lt;p&gt;며칠 뒤, 지금 카드에 하나를 더 보태거나 다른 카드를 추가로 사는 게 이득인지 다시 검토할 일이 생겼습니다. 이번엔 외부 벤치마크 수치를 여러 개 모아 비교표까지 만들었습니다.&lt;/p&gt;

&lt;p&gt;그런데 표를 다시 훑어보다가 근거 칸이 통째로 비어 있는 항목을 발견했습니다. 그 항목이 인용한 원본 자료의 핵심 근거 네 가지 중 세 가지가, 이미 우리 쪽 실측으로 반증된 내용이었습니다.&lt;/p&gt;

&lt;p&gt;더 근본적인 문제도 함께 드러났습니다. 카드 간 처리 속도 차이를 메모리 대역폭 비율로 환산해서 비교하고 있었는데, 이 환산식은 지금 쓰는 속도 향상 기법을 켠 상태에서는 성립하지 않았습니다. 이 기법을 켜면 다음 토큰 계산이 대역폭에 발목 잡히지 않는 구간이 생기는데, 환산식은 그걸 반영하지 못해서 한쪽 카드는 과소평가하고 다른 쪽은 과대평가하는 식으로 양방향으로 틀렸습니다.&lt;/p&gt;

&lt;p&gt;같은 날 외부에서 새로 찾은 실측 자료를 대조해보니 또 다른 결함이 나왔습니다. 참고한 외부 벤치마크 대부분이 "입력 문맥 길이"를 통제하지 않은 채 측정된 자료였습니다. 문맥이 길어질수록 처리 속도가 크게 떨어지는 걸 감안하지 않은 숫자를, 실제로는 훨씬 긴 문맥을 쓰는 우리 작업에 그대로 대입하고 있었던 겁니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  세 번째 확인 — 다른 사람에게 검토를 맡겼더니 또 고칠 점이 나왔다
&lt;/h2&gt;

&lt;p&gt;이 정도로 정리했다고 생각했는데, 다른 AI에게 같은 자료를 다시 검토시켰더니 세 군데를 더 정정받았습니다.&lt;/p&gt;

&lt;p&gt;하나는 모델 구조 차이였습니다. 지금 운영 중인 모델과 검토 후보 카드를 비교한 수치가, 실제로는 계산량이 훨씬 큰 다른 구조의 모델 기준이었습니다. 결론(추가 구매 보류)은 바뀌지 않았지만, "근거 없음"이라는 판정 이유 자체를 고쳐 써야 했습니다.&lt;/p&gt;

&lt;p&gt;다른 하나는 오독의 방향이었습니다. 순수 연산 성능(속도 향상 기법을 끈 상태)끼리 비교할 때는 대역폭 환산식이 여전히 유효했는데, 앞서 "환산식이 통째로 무효"라고 단정했던 게 과했습니다. 무효인 건 그 기법을 켠 구간에 한정된 얘기였습니다.&lt;/p&gt;

&lt;p&gt;세 번째는 아예 근거 자체가 무너진 항목이었습니다. 특정 언어가 섞여 나오는 현상을 양자화 정밀도 탓으로 돌렸는데, 같은 정밀도로 돌리는 다른 모델에서는 그 현상이 전혀 없었습니다. 이미 손에 있던 반증 자료를 놓치고 있었던 겁니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  결국 정리한 방법 — 저비용 직접 실험
&lt;/h2&gt;

&lt;p&gt;검토를 반복할수록 외부 벤치마크를 더 정교하게 읽는 쪽으로 힘을 쏟게 됐는데, 매번 새로운 결함이 나왔습니다. 그래서 방향을 바꿨습니다.&lt;/p&gt;

&lt;p&gt;이미 주문해뒀던 고가의 추가 카드는 반품하기로 하고, 대신 지금 가진 카드로 비용이 거의 안 드는 직접 실험(정밀도를 한 단계 올렸을 때 실제로 무엇이 얼마나 좋아지는지를 며칠 안에 직접 재보는 실험)부터 하기로 했습니다.&lt;/p&gt;

&lt;p&gt;카탈로그와 외부 벤치마크를 아무리 정교하게 대조해도, 우리 작업 조건(문맥 길이·모델 구조·속도 향상 기법 사용 여부)과 정확히 일치하는 자료는 애초에 존재하지 않았습니다. 반면 직접 실험은 조건이 항상 우리 작업과 정확히 같습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  일반화한 교훈
&lt;/h2&gt;

&lt;p&gt;돌아보면 첫 번째 오판과 두 번째 오판은 겉으로는 비슷해 보여도 뿌리가 달랐습니다. 첫 번째는 참고 자료를 잘못 짝지은 단순 오독이었고, 두 번째는 참고 자료 자체는 맞았지만 우리 작업 조건과 안 맞는 걸 그대로 대입한 문제였습니다.&lt;/p&gt;

&lt;p&gt;두 오판을 겪고 나서 하드웨어나 제품을 비교할 때 스스로 점검하는 항목이 늘었습니다. 인용하는 수치가 정말 비교하려는 대상과 같은 조건(같은 모델 구조, 같은 부가 기법 사용 여부, 같은 작업 조건)에서 나온 값인지부터 확인합니다.&lt;/p&gt;

&lt;p&gt;비교 공식이나 환산식을 쓸 때는, 그 공식이 성립하는 전제 조건이 무엇인지 먼저 확인합니다. 조건이 하나라도 바뀌면(이번엔 속도 향상 기법 사용 여부) 공식 자체가 깨질 수 있다는 걸 이번에 배웠습니다.&lt;/p&gt;

&lt;p&gt;그리고 판정이 짧은 시간에 두 번 이상 뒤집힌다면, 그건 세 번째 검토가 필요하다는 신호로 받아들이기로 했습니다. 카탈로그 비교로 답이 안 나올 때는, 계속 더 정교하게 비교하기보다 저비용으로 직접 재보는 쪽이 더 빨리 답에 닿았습니다.&lt;/p&gt;

</description>
      <category>softwarequality</category>
      <category>postmortem</category>
      <category>debugging</category>
      <category>quant</category>
    </item>
    <item>
      <title>Comparing GPU Performance After a Hardware Swap - Why the Verdict Flipped Twice</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Sat, 22 Aug 2026 12:18:23 +0000</pubDate>
      <link>https://dev.to/finaltype/comparing-gpu-performance-after-a-hardware-swap-why-the-verdict-flipped-twice-1ppa</link>
      <guid>https://dev.to/finaltype/comparing-gpu-performance-after-a-hardware-swap-why-the-verdict-flipped-twice-1ppa</guid>
      <description>&lt;p&gt;&lt;em&gt;My first direction call was wrong, and my later purchase call was wrong too, for two completely different reasons — the root cause both times was how I was reading benchmarks&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the English version of a post originally written in Korean for my &lt;a href="https://finaltype.github.io/quant-blog/en/architecture/01_project_architecture/" rel="noopener noreferrer"&gt;algorithmic trading system devlog&lt;/a&gt;(new tab).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I swapped my GPU for one from a different brand, and had to pick which execution mode to run it in.&lt;/p&gt;

&lt;p&gt;There were two candidate modes, and I judged one clearly better at first. That call turned out to be backwards, and the verdict flipped again after that. Writing this down to work out why it kept happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  First verdict — the direction was backwards
&lt;/h2&gt;

&lt;p&gt;Moving to the new card meant choosing between two execution modes. I looked at some external reference numbers to compare them, and concluded that the speedup technique I was already relying on (predicting several tokens ahead instead of one at a time) simply didn't work well on one of the two modes.&lt;/p&gt;

&lt;p&gt;If that conclusion held, dropping the technique was the right call. But looking again the next day, it was the opposite. The technique wasn't unsupported at all — it produced garbage output, which is a correctness problem, not a performance one.&lt;/p&gt;

&lt;p&gt;The root cause was a misread reference number. One of the two figures I'd cited turned out to belong to a different kind of model (one with far less active computation per token), and the naming was close enough that I mistook it for the model I was actually running. Working backwards through the math showed that number was physically impossible for the model structure I was using — that's what exposed the misread.&lt;/p&gt;

&lt;p&gt;Once the direction was corrected, I went ahead and deployed. A single card ended up matching or beating the throughput of the two older cards it replaced, and I tuned batch settings on top and confirmed the numbers directly. At that point the verdict looked settled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Second verdict — flipped again while evaluating an additional purchase
&lt;/h2&gt;

&lt;p&gt;A few days later, I had to re-evaluate whether adding another card, or a different card entirely, was worth it. This time I gathered several external benchmark numbers into a comparison table.&lt;/p&gt;

&lt;p&gt;Going back over the table, I found one row where the evidence column was completely empty. Three of the four core arguments in the source material that row cited had already been contradicted by our own measurements.&lt;/p&gt;

&lt;p&gt;A deeper problem surfaced too. I'd been converting throughput differences between cards using a memory-bandwidth ratio, but that conversion doesn't hold once the speedup technique is turned on. With it on, next-token computation stops being purely bandwidth-bound in places, and the ratio formula didn't account for that — it ended up underestimating one card and overestimating the other, wrong in both directions.&lt;/p&gt;

&lt;p&gt;Cross-checking against newly found external measurements the same day turned up yet another flaw. Most of the external benchmarks I'd been citing hadn't controlled for input context length at all. Throughput drops sharply as context grows, and I'd been applying numbers that ignored that to a workload that actually runs on much longer context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Third check — a second review caught still more
&lt;/h2&gt;

&lt;p&gt;I thought that was thorough enough, but having a different AI review the same material turned up three more corrections.&lt;/p&gt;

&lt;p&gt;One was a model-structure mismatch. A figure I'd used to compare the currently running model against a candidate card actually came from a much heavier model structure. The conclusion (hold off on buying) didn't change, but the stated reason for it had to be rewritten.&lt;/p&gt;

&lt;p&gt;Another was about the direction of my own correction. For non-speculative comparisons — with the speedup technique off — the bandwidth ratio formula still held; I'd overcorrected by declaring it invalid across the board. It was only invalid for the regime where that technique is on.&lt;/p&gt;

&lt;p&gt;The third was an argument that collapsed entirely. I'd blamed a language-mixing glitch on quantization precision, but a different model running at the exact same precision showed none of it. The counter-evidence had been sitting in hand the whole time.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I finally settled it — a cheap direct experiment
&lt;/h2&gt;

&lt;p&gt;Every extra round of review meant reading external benchmarks more carefully, and every round turned up a new flaw. So I changed approach.&lt;/p&gt;

&lt;p&gt;I returned the expensive extra card I'd already ordered, and instead planned a low-cost direct experiment on the hardware I already had — actually measuring what a one-step precision upgrade buys, within a few days, instead of arguing from a catalog.&lt;/p&gt;

&lt;p&gt;No matter how carefully I cross-referenced external benchmarks, none of them matched our actual working conditions (context length, model structure, whether the speedup technique was on) exactly. A direct experiment always does, by construction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general lesson
&lt;/h2&gt;

&lt;p&gt;Looking back, the two mistakes looked similar on the surface but had different roots. The first was a simple misattribution — citing the wrong source for the wrong model. The second was citing the right source but applying it under conditions it was never measured under.&lt;/p&gt;

&lt;p&gt;After both, I added a few checks to how I evaluate hardware or product comparisons. First: does the number I'm citing actually come from the same conditions I'm comparing against — same model structure, same auxiliary techniques enabled, same workload?&lt;/p&gt;

&lt;p&gt;Second, whenever I use a conversion formula, I check what assumptions it depends on. One condition changing — in this case, whether a speedup technique is on — was enough to break the formula, and I didn't see that until it already had.&lt;/p&gt;

&lt;p&gt;And now, if a verdict flips more than once in a short window, I treat that as a signal that it needs a third, independent review rather than a fourth pass at the same sources. When catalog comparisons stop converging, a cheap direct measurement gets to the answer faster than reading benchmarks more carefully.&lt;/p&gt;

</description>
      <category>softwarequality</category>
      <category>postmortem</category>
      <category>debugging</category>
      <category>quant</category>
    </item>
    <item>
      <title>AI 리포트 수치 검증기 - 자기정합 검사만으론 못 잡는 오류들</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Sat, 22 Aug 2026 12:12:17 +0000</pubDate>
      <link>https://dev.to/finaltype/ai-ripoteu-suci-geomjeunggi-jagijeonghab-geomsamaneuron-mos-jabneun-oryudeul-c7h</link>
      <guid>https://dev.to/finaltype/ai-ripoteu-suci-geomjeunggi-jagijeonghab-geomsamaneuron-mos-jabneun-oryudeul-c7h</guid>
      <description>&lt;p&gt;&lt;em&gt;문서 안에서는 앞뒤가 맞는데 원본 데이터와는 다른 숫자 — 그런 오류를 잡으려고 검증기를 새로 설계했습니다&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;자동매매 시스템은 매일 종목마다 AI가 작성한 리포트를 만들어냅니다. 사람이 그걸 하나하나 다 읽고 숫자를 확인할 수는 없습니다.&lt;/p&gt;

&lt;p&gt;리포트 안에는 가격, 재무 수치 같은 구체적인 숫자가 많이 들어갑니다. 그 숫자가 틀리면 판단 근거 자체가 틀린 셈이라, 오래전부터 검사 로직을 붙여두긴 했습니다.&lt;/p&gt;

&lt;p&gt;그런데 이번에 그 검사 로직을 다시 들여다볼 일이 생겼습니다. "검사하고 있다"는 게 정말 "확인해야 할 걸 확인하고 있다"는 뜻인지 스스로 의심이 들어서였습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  기존 검사가 실제로 하던 일
&lt;/h2&gt;

&lt;p&gt;기존에 있던 검사는 리포트 문서 "안에서" 숫자들이 서로 맞는지를 봤습니다. 이를테면 리포트 앞부분에 쓴 숫자와 뒷부분에 다시 나온 같은 숫자가 일치하는지, 그런 자기정합(self-consistency) 검사였습니다.&lt;/p&gt;

&lt;p&gt;이 방식은 리포트가 자기 자신과 모순되는지는 잡아낼 수 있습니다. 하지만 리포트가 처음부터 원본 데이터를 잘못 옮겨 적었는데 그 잘못을 문서 전체에서 일관되게 반복했다면, 이 검사는 아무것도 못 봅니다.&lt;/p&gt;

&lt;p&gt;예를 들어 원본 데이터의 숫자를 다른 단위로 환산하면서 자릿수를 하나 잘못 옮겼다고 해봅시다. 리포트 안에서 그 잘못된 숫자를 계속 똑같이 인용하면, 문서 내부적으로는 완벽하게 앞뒤가 맞습니다. 자기정합 검사 입장에서는 "문제 없음"입니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  그래서 다른 검증기를 새로 설계했다
&lt;/h2&gt;

&lt;p&gt;필요했던 건 리포트 문장을 원본 입력 데이터와 직접 대조하는 검증기였습니다. 문서 내부가 아니라 문서 바깥의 참값과 비교하는, 완전히 다른 종류의 검사입니다.&lt;/p&gt;

&lt;p&gt;리포트에 등장하는 숫자는 크게 두 갈래였습니다. 하나는 가격·기술적 지표처럼 캔들 데이터에서 나온 숫자, 다른 하나는 매출·시가총액 같은 재무 수치였습니다. 이 둘은 원본 데이터의 성격이 달라서 검증기도 가격 축과 금액 축, 두 갈래로 나눠서 설계했습니다.&lt;/p&gt;

&lt;p&gt;이 구분이 중요했던 이유는 나중에 드러났습니다. 가격 축은 원본(캔들)이 매일 계속 쌓이니 언제든 소급해서 대조할 수 있습니다. 반면 금액 축은 그날 리포트에 실제로 입력된 재무 데이터 블록만 원본으로 남기 때문에, 지나간 날짜는 원본 자체가 없어서 소급 대조가 안 됩니다. 이 비대칭이 나중에 오류 규모를 재는 방식 자체를 바꿔놨습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  첫 버전은 너무 많이 울었다
&lt;/h2&gt;

&lt;p&gt;가격 축 검증기의 첫 버전은 만들자마자 오탐이 폭증했습니다. 대상 종목 대부분에서 뭔가 걸렸는데, 들여다보니 실제 오류가 아니라 우연의 일치였습니다.&lt;/p&gt;

&lt;p&gt;원인은 캔들 데이터가 워낙 많다는 데 있었습니다. 참값 후보가 수백 개나 되는 상태에서 느슨한 오차 범위로 대조하면, 리포트에 나온 웬만한 숫자는 그 후보들 중 하나와 우연히 비슷하게 맞아떨어집니다.&lt;/p&gt;

&lt;p&gt;그래서 조건을 세 겹으로 조였습니다. 정확히 일치하는 값만 인정하고, 대조 범위를 최근 구간으로 좁히고, 같은 유형의 오류가 여러 건 함께 나올 때만 신뢰하는 식이었습니다. 결과적으로 재현율을 낮추는 대신 정밀도를 크게 높인 셈입니다.&lt;/p&gt;

&lt;p&gt;이 트레이드오프는 의도적으로 선택한 겁니다. 무인으로 돌아가는 시스템에서 오탐이 계속 쌓이면 사람이 알림 자체를 무시하게 됩니다. 이럴 바엔 드물게 놓치더라도 조용한 검증기가 낫다고 판단했습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  배선하자마자 드러난 것들
&lt;/h2&gt;

&lt;p&gt;가격 축과 금액 축 검증기를 실제 운영 파이프라인에 배선하고, 프로덕션 데이터로 첫 기저선을 쟀습니다. 가격 축은 오류가 거의 없었는데, 이건 기존에 알던 관측과 일치했습니다.&lt;/p&gt;

&lt;p&gt;문제는 금액 축이었습니다. 하루치 리포트만 봐도 절반이 넘는 종목에서 재무 수치 오류가 나왔습니다. 실제 사례를 하나 확인해보니, 원본 데이터의 숫자를 다른 단위로 환산하는 과정에서 뒷자리를 잘못 잘라내는 바람에 실제보다 값이 크게 부풀려진 채로 리포트에 실려 있었습니다. 문서 안에서는 그 잘못된 숫자를 일관되게 썼기 때문에, 기존 자기정합 검사는 그동안 이걸 계속 통과시켜왔습니다.&lt;/p&gt;

&lt;p&gt;배선 과정에서 예상 못 한 버그도 두 개 나왔습니다. 검증기가 돌려주는 결과 자료구조의 모양을 호출하는 쪽이 잘못 가정하고 있었는데, 이게 실제 프로덕션 데이터로 돌려보기 전까지는 드러나지 않았습니다. 하나는 정상적으로 오류를 잡아낸 결과가 "도구 자체의 오류"로 잘못 기록될 뻔했고, 다른 하나는 결과를 저장하는 과정 자체가 조용히 실패해서 그날 밤 산출물이 통째로 사라질 뻔했습니다. 둘 다 소스 코드만 읽어서는 못 찾고, 실제로 돌려봐야만 드러나는 종류의 결함이었습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  45일을 되짚어보니
&lt;/h2&gt;

&lt;p&gt;배선 직후의 하루치 결과만으로는 이게 우연인지 일상인지 알 수 없었습니다. 그래서 과거 45일치 리포트를 다시 돌려서 같은 검사를 소급 적용해봤습니다.&lt;/p&gt;

&lt;p&gt;가격 축은 원본이 매일 남아 있어서 소급이 쉬웠습니다. 지난 25거래일 전 구간을 다시 돌려도 오류는 거의 나오지 않았고, 이건 프로덕션에서 본 것과 같은 결론이었습니다.&lt;/p&gt;

&lt;p&gt;금액 축은 사정이 달랐습니다. 원본 재무 데이터 블록이 하루치만 남아 있어서 정확한 소급 대조가 불가능했습니다. 대신 "물리적으로 있을 수 없는 금액"(국내 증시 전체 시가총액을 넘는 값 같은)을 대리 지표로 써서 오류를 대신 셌습니다.&lt;/p&gt;

&lt;p&gt;그 결과, 지난 45일 동안 이 유형의 오류가 하루도 빠짐없이 나왔습니다. 하루하루 비율은 들쭉날쭉했지만 오류가 아예 없는 날은 단 하루도 없었습니다.&lt;/p&gt;

&lt;p&gt;이 수치는 하한선이라는 점을 분명히 해야 합니다. 대리 지표는 값을 크게 부풀리는 방향의 오류만 잡을 수 있는 구조라서, 원본과 직접 대조가 가능했던 하루치를 기준으로 비교해보니 대리 지표가 잡아낸 건 실제 오류의 일부에 불과했습니다. 실제 오류 규모는 이보다 훨씬 컸을 가능성이 큽니다.&lt;/p&gt;

&lt;p&gt;이 소급 조사를 하는 과정에서 검증기 자체의 또 다른 한계도 하나 발견했습니다. 특정 배율로 단위를 잘못 바꾼 오류 패턴을 이 검증기가 애초에 인식하는 목록에 넣어두지 않았던 겁니다. 검증기를 만드는 과정 자체가, 검증기의 사각지대를 계속 찾아나가는 과정이었던 셈입니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  기존 검사는 왜 이 클래스를 구조적으로 못 봤나
&lt;/h2&gt;

&lt;p&gt;정리하면 이렇습니다. 기존 자기정합 검사는 "리포트가 스스로와 모순되는가"만 물었습니다. 원본 데이터를 다시 들여다보는 절차 자체가 없었습니다.&lt;/p&gt;

&lt;p&gt;이번에 찾은 오류 클래스는 원본 숫자를 리포트로 옮겨 적는 과정에서 단위나 자릿수를 잘못 바꾼 것이었습니다. 그 잘못된 숫자를 리포트 안에서 일관되게 반복하기만 하면, 문서 내부에는 아무 모순도 생기지 않습니다.&lt;/p&gt;

&lt;p&gt;그러니까 "검사 로직이 돌고 있다"는 사실과 "이 오류를 검사하고 있다"는 사실은 서로 다른 문장이었습니다. 새 검증기를 만들어서 실제로 돌려보기 전까지는, 이런 오류가 매일 나오고 있다는 사실 자체를 몰랐습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  일반화하면
&lt;/h2&gt;

&lt;p&gt;AI가 만들어내는 리포트나 텍스트를 검증할 일이 있다면, 이번 경험에서 몇 가지를 챙길 만합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;자기정합 검사와 원본대조 검사는 다른 걸 잰다.&lt;/strong&gt; 문서가 스스로 모순되지 않는다는 것과 문서가 사실에 부합한다는 것은 별개의 명제입니다. 둘 중 하나만 있으면 절반만 검증하는 셈입니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;검증기를 만들었다고 끝이 아니다.&lt;/strong&gt; "이 검증기가 구조적으로 절대 못 보는 오류의 모양이 뭔가"를 스스로 물어야 합니다. 이번에도 검증기를 만드는 와중에 그 검증기 자신의 사각지대를 또 하나 찾아냈습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;오탐과 누락의 트레이드오프는 명시적으로 정해야 한다.&lt;/strong&gt; 특히 사람이 매번 확인하지 못하는 무인 운영 환경이라면, 오탐이 쌓여서 알림 자체를 무시하게 되는 상황이 더 나쁠 수 있습니다. 어느 쪽을 우선할지 이유와 함께 정해두는 게 낫습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;실데이터로 배선해서 돌려보기 전엔 안심할 수 없다.&lt;/strong&gt; 이번에 나온 인터페이스 관련 버그 두 개는 소스 코드 리뷰만으로는 안 보이고, 실제 프로덕션 데이터를 흘려보내야만 드러나는 종류였습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;참값이 일부만 있을 때는 대리 지표를 쓰되, 그게 하한이라는 걸 명시해야 한다.&lt;/strong&gt; 대리 지표가 실제 규모를 과소평가할 수 있다는 걸 알고 있어야, 그 숫자를 안심의 근거로 잘못 쓰지 않습니다.&lt;/p&gt;

&lt;p&gt;검증기 하나를 새로 붙일 때마다, "이 검증기가 지금 뭘 못 보고 있을까"를 다음 점검 목록에 올려두려고 합니다.&lt;/p&gt;

</description>
      <category>softwarequality</category>
      <category>postmortem</category>
      <category>debugging</category>
      <category>quant</category>
    </item>
    <item>
      <title>Building a Numeric Verifier for AI Report Output</title>
      <dc:creator>finaltype</dc:creator>
      <pubDate>Sat, 22 Aug 2026 12:12:16 +0000</pubDate>
      <link>https://dev.to/finaltype/building-a-numeric-verifier-for-ai-report-output-4254</link>
      <guid>https://dev.to/finaltype/building-a-numeric-verifier-for-ai-report-output-4254</guid>
      <description>&lt;p&gt;&lt;em&gt;A self-consistency check can't catch a number that's internally coherent but simply wrong — so I designed a different kind of verifier&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the English version of a post originally written in Korean for my &lt;a href="https://finaltype.github.io/quant-blog/en/architecture/01_project_architecture/" rel="noopener noreferrer"&gt;algorithmic trading system devlog&lt;/a&gt;(new tab).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My automated trading system generates an AI-written report for every ticker, every day. There's no way I can read every single one and check every number by hand.&lt;/p&gt;

&lt;p&gt;Those reports are dense with concrete figures — prices, financial numbers. If a number is wrong, the reasoning built on top of it is wrong too, so I'd had some kind of check on report numbers in place for a while.&lt;/p&gt;

&lt;p&gt;Then I had reason to look at that check again. I wasn't sure "there's a check running" actually meant "the thing I care about is being checked."&lt;/p&gt;

&lt;h2&gt;
  
  
  What the existing check actually did
&lt;/h2&gt;

&lt;p&gt;The check I already had looked at whether numbers agreed with each other &lt;em&gt;inside&lt;/em&gt; a single report. If a figure appeared early in the report and again later, it verified the two mentions matched — a self-consistency check.&lt;/p&gt;

&lt;p&gt;That catches a report contradicting itself. It does nothing if the report got a number wrong from the very start and then just repeated that same wrong number consistently throughout.&lt;/p&gt;

&lt;p&gt;Say a number from the source data gets converted to a different unit and a digit gets dropped in the process. If the report keeps citing that wrong number the same way everywhere, the document is internally coherent from end to end. To a self-consistency checker, that's a clean pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I designed a different kind of verifier
&lt;/h2&gt;

&lt;p&gt;What I actually needed was something that compared report sentences directly against the source data — not internal consistency, but agreement with ground truth outside the document.&lt;/p&gt;

&lt;p&gt;Numbers in these reports fell into two rough categories: price and technical-indicator figures pulled from candle data, and financial figures like revenue or market cap. Because the source data behind each is shaped differently, I split the verifier into two separate axes — a price axis and an amount axis.&lt;/p&gt;

&lt;p&gt;That split mattered more than I expected. Candle data (the ground truth for the price axis) keeps accumulating every trading day, so it can always be checked retroactively. The ground truth for the amount axis, on the other hand, only exists as the financial data block that was actually fed into that day's report — once a day passes, there's no ground truth left to retroactively check against. That asymmetry ended up shaping how I later had to measure the scale of the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first version cried wolf
&lt;/h2&gt;

&lt;p&gt;The first version of the price-axis verifier fired constantly the moment I built it. Most tickers tripped it, but on inspection, none of it was real — it was all coincidence.&lt;/p&gt;

&lt;p&gt;The problem was volume: candle data gives you hundreds of candidate reference values. With a loose tolerance window, almost any number in a report will happen to land close to one of those candidates by chance alone.&lt;/p&gt;

&lt;p&gt;I tightened it with three layers: only exact matches count, the comparison window is limited to recent candles, and a flag only counts as real when the same error pattern shows up in a cluster rather than in isolation. Net effect: lower recall, much higher precision.&lt;/p&gt;

&lt;p&gt;That trade-off was deliberate. In an unattended system, a verifier that keeps crying wolf eventually gets ignored altogether. I'd rather miss the occasional isolated error than train myself to tune out the alerts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What showed up the moment it was wired in
&lt;/h2&gt;

&lt;p&gt;Once both axes were wired into the actual production pipeline, I measured the first real baseline against production data. The price axis came back nearly clean, matching what I'd already observed informally.&lt;/p&gt;

&lt;p&gt;The amount axis was the opposite story. More than half the tickers in a single day's reports had a financial-figure error. Looking at one concrete case, a source number had been converted to a different unit and the trailing digits got dropped along the way — inflating the reported value well past the true figure. Because the report cited that wrong number consistently throughout, the old self-consistency check had been letting it through this whole time.&lt;/p&gt;

&lt;p&gt;Wiring it in also surfaced two bugs I hadn't anticipated. The caller was making a wrong assumption about the shape of the data structure the verifier returned, and this only broke once real production data ran through it. One version of the bug would have silently logged genuine detections as "tool errors." The other would have silently crashed the step that writes results out, wiping an entire night's output. Neither was visible from reading the source — only running it for real exposed them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking back 45 days
&lt;/h2&gt;

&lt;p&gt;A single day's result right after wiring wasn't enough to tell whether this was a fluke or the norm. So I re-ran the same check retroactively across the past 45 days of reports.&lt;/p&gt;

&lt;p&gt;The price axis was easy to check retroactively, since its ground truth persists. Re-running it across the past several weeks of trading days turned up almost nothing — consistent with what production had already shown.&lt;/p&gt;

&lt;p&gt;The amount axis was harder. Since the ground-truth financial data block only survives for a single day, I couldn't do a direct retroactive comparison. Instead I used "a physically impossible amount" — a figure that exceeds the entire domestic stock market's total capitalization — as a proxy signal for an error.&lt;/p&gt;

&lt;p&gt;The result: this class of error showed up every single day across all 45 days, without exception. The daily rate varied, but there wasn't one clean day in the whole window.&lt;/p&gt;

&lt;p&gt;That number has to be read as a floor, not a full count. The proxy can only catch errors that inflate a value, so on the one day I could do a direct ground-truth comparison, the proxy caught only a fraction of what direct comparison found. The real error rate was almost certainly much higher.&lt;/p&gt;

&lt;p&gt;This retrospective sweep also surfaced another gap in the verifier itself — a specific unit-scaling error pattern it hadn't been built to recognize at all. Building the verifier turned out to be an ongoing process of finding the verifier's own blind spots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the old check was structurally blind to this class
&lt;/h2&gt;

&lt;p&gt;To sum it up: the old self-consistency check only ever asked "does the report contradict itself?" It never went back and looked at the source data again.&lt;/p&gt;

&lt;p&gt;The error class I found here came from mistranscribing a unit or a digit count while copying a number from source into the report. As long as the report then repeated that wrong number consistently, nothing inside the document ever contradicted itself.&lt;/p&gt;

&lt;p&gt;"There's a check running" and "this specific error is being checked for" turned out to be two different sentences. Until I built the new verifier and actually ran it, I had no idea this error had been happening every day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generalizing it
&lt;/h2&gt;

&lt;p&gt;If you're verifying output from an AI-generated report or any LLM pipeline, a few things from this are worth carrying over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-consistency checks and ground-truth checks measure different things.&lt;/strong&gt; A document not contradicting itself and a document being factually correct are separate claims. Having only one of the two means you're only half-verified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building the verifier isn't the finish line.&lt;/strong&gt; Keep asking what error shape it's structurally incapable of seeing. Even while building this one, I found another blind spot in it along the way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decide the false-positive/false-negative trade-off on purpose.&lt;/strong&gt; In an unattended system especially, a verifier that cries wolf too often does more damage than one that occasionally stays quiet. Pick a side deliberately and write down why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing is safe until it's wired into real data.&lt;/strong&gt; The two interface bugs here were invisible on code review and only surfaced once real production data flowed through the pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When ground truth only partially exists, a proxy metric is fine — as long as you label it a floor.&lt;/strong&gt; Knowing the proxy likely undercounts keeps you from mistaking that number for reassurance.&lt;/p&gt;

&lt;p&gt;Every time I bolt on a new verifier now, "what is this thing still missing" goes straight onto the next checklist.&lt;/p&gt;

</description>
      <category>softwarequality</category>
      <category>postmortem</category>
      <category>debugging</category>
      <category>quant</category>
    </item>
  </channel>
</rss>
