<?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: oji - building AI in public</title>
    <description>The latest articles on DEV Community by oji - building AI in public (@masaoshimadaopen).</description>
    <link>https://dev.to/masaoshimadaopen</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%2F4013207%2F62889ff7-f41e-4077-9836-3fafe971b8ce.jpg</url>
      <title>DEV Community: oji - building AI in public</title>
      <link>https://dev.to/masaoshimadaopen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/masaoshimadaopen"/>
    <language>en</language>
    <item>
      <title>My custom filter showed "zero false positives"! Then I realized my validation logic was flawed, letting all the garbage through.</title>
      <dc:creator>oji - building AI in public</dc:creator>
      <pubDate>Thu, 10 Sep 2026 23:30:18 +0000</pubDate>
      <link>https://dev.to/masaoshimadaopen/my-custom-filter-showed-zero-false-positives-then-i-realized-my-validation-logic-was-flawed-6o</link>
      <guid>https://dev.to/masaoshimadaopen/my-custom-filter-showed-zero-false-positives-then-i-realized-my-validation-logic-was-flawed-6o</guid>
      <description>&lt;p&gt;Hey there, it's your "Oji" (38-year-old AI/quant dev on the side). During the week, I'm a regular company employee; on weekends, I tinker with AI agents and automated trading bots.&lt;/p&gt;

&lt;p&gt;Recently, I was building a filter to automatically extract "AI-related companies" from a market data list, based on their business descriptions. The mechanism is simple: it scores companies based on hits of predefined positive keywords (e.g., "machine learning," "natural language processing") and negative keywords (e.g., "gaming," "entertainment").&lt;/p&gt;

&lt;p&gt;To improve the filter's accuracy, I set up a test to tune the threshold (how many positive keyword hits qualify a company). I prepared lists of positive examples (actual AI companies) and negative examples (non-AI companies) to see how well the filter classified them—standard stuff.&lt;/p&gt;

&lt;p&gt;Then I ran the test, and the results were mind-blowing.&lt;/p&gt;

&lt;p&gt;"Zero false positives, no matter the threshold I tried."&lt;/p&gt;

&lt;p&gt;For a moment, I thought, "Is my filter a genius?" Zero false positives is usually impossible. But seconds later, I cooled down. Results that are &lt;em&gt;too perfect&lt;/em&gt; usually mean something is wrong.&lt;/p&gt;

&lt;p&gt;Sure enough, when I manually reviewed the list of companies that passed the filter, I found obvious non-AI companies like gaming studios mixed in. My filter was letting garbage through, yet the test reported "no issues." The despair I felt realizing this late on a weekday night was quite something.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why "Zero False Positives"?
&lt;/h3&gt;

&lt;p&gt;The root cause wasn't a bug in the code itself, but a design flaw in the validation logic.&lt;/p&gt;

&lt;p&gt;Basic accuracy validation involves looking at two metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;False Positive (FP)&lt;/strong&gt;: A non-AI company incorrectly classified as an AI company.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;False Negative (FN)&lt;/strong&gt;: An AI company incorrectly classified as a non-AI company.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The issue I encountered was with FP. To count FPs, the definition of &lt;strong&gt;negative examples&lt;/strong&gt; (companies that are "not AI companies") is crucial.&lt;/p&gt;

&lt;p&gt;And here was my definition of a negative example:&lt;/p&gt;

&lt;p&gt;"A company with 0 positive keyword hits AND 2 or more negative keyword hits."&lt;/p&gt;

&lt;p&gt;This was the source of all evil. This definition, seemingly sound at first glance, contained a self-contradiction.&lt;/p&gt;

&lt;p&gt;My filter's rule for classifying a company as "passing (AI-related)" was: "2 or more positive keyword hits."&lt;/p&gt;

&lt;p&gt;You probably see it now.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Filter's passing condition&lt;/strong&gt;: &lt;code&gt;positive_hits &amp;gt;= 2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Test's negative example condition&lt;/strong&gt;: &lt;code&gt;positive_hits == 0&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These two conditions can &lt;em&gt;never&lt;/em&gt; be true simultaneously. A company classified as "passing" by the filter &lt;em&gt;already&lt;/em&gt; has &lt;code&gt;positive_hits &amp;gt;= 2&lt;/code&gt;, so it can &lt;em&gt;never&lt;/em&gt; fit the negative example definition of &lt;code&gt;positive_hits == 0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In essence, when trying to count "negative examples that were incorrectly classified as passing (i.e., false positives)," the structure itself made it impossible for any company to be both a "negative example" and "passing." Of course, false positives would always be zero.&lt;/p&gt;

&lt;p&gt;Translating the concept to code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Filter rule: Pass if positive keyword count &amp;gt;= 2&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;is_theme_company&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;positive_hits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;positive_hits&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Validation's negative example definition (buggy)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;is_negative_example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;positive_hits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;negative_hits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Only consider companies with zero positive hits as negative examples&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;positive_hits&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;negative_hits&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Validation process&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;false_positives&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;company&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;all_companies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;is_selected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;is_theme_company&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pos_hits&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// If it's a negative example but selected, count as FP&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;is_selected&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;is_negative_example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pos_hits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;neg_hits&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;false_positives&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// With this logic, is_selected=true (pos_hits&amp;gt;=2) and is_negative_example=true (pos_hits==0)&lt;/span&gt;
&lt;span class="c1"&gt;// can never both be true, so false_positives will always be zero.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test was too tightly coupled to the logic it was supposed to validate, preemptively deciding the outcome. It was a brutal rookie mistake.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I Fixed It
&lt;/h3&gt;

&lt;p&gt;Once I understood the cause, the fix was simple.&lt;/p&gt;

&lt;p&gt;I changed the definition of negative examples to be independent of keyword hit counts. Specifically, I manually selected dozens of companies that were unequivocally &lt;em&gt;not&lt;/em&gt; AI companies (e.g., food manufacturers, apparel brands, construction companies) and created a fixed "negative example list."&lt;/p&gt;

&lt;p&gt;Running the test again with this list, false positives, as expected, came pouring out. Finally, the numbers showed that the filter was indeed picking up many irrelevant companies. This was the real starting point for tuning.&lt;/p&gt;

&lt;h3&gt;
  
  
  My Takeaway
&lt;/h3&gt;

&lt;p&gt;The lessons from this failure are significant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"The test passed" only means something if the &lt;em&gt;test itself&lt;/em&gt; is correct.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Finding code bugs with tests is fundamental, but you must always question the possibility that the test logic itself is flawed. Especially when validating rules you've created yourself, there's a risk that the validation logic gets dragged by the tested logic, unconsciously leading to "conclusion-first" tests.&lt;/p&gt;

&lt;p&gt;When you get "too perfect results," first question your own assumptions. This is a crucial reminder I'll engrave into my mind.&lt;/p&gt;

&lt;p&gt;This story also applies to backtesting automated trading bots. When a backtest shows unusually good performance, it's rarely because you've discovered a brilliant strategy; it's usually because you're looking at future data (leakage) or not accounting for fees and slippage.&lt;/p&gt;

&lt;p&gt;I'm logging embarrassing failures like this as part of building in public. Hopefully, it helps someone else in their solo dev journey.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build and run small Python systems — trading bots, RAG APIs, scheduled automation — and write up whatever breaks along the way.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If a provider-agnostic RAG Q&amp;amp;A API is useful to you, mine is MIT-licensed on GitHub: &lt;a href="https://github.com/masaoshimadaOpen/rag-faq-api" rel="noopener noreferrer"&gt;rag-faq-api&lt;/a&gt;. It runs and passes its full test suite **with no API key&lt;/em&gt;* (offline stub LLM + hashing embedder), swaps to Claude / Gemini / OpenAI via one env var, and ships a retrieval-quality harness (Hit@k / MRR / Recall@k) with a chunking sweep.*&lt;/p&gt;

</description>
      <category>ai</category>
      <category>debugging</category>
      <category>machinelearning</category>
      <category>automation</category>
    </item>
    <item>
      <title>米国企業の分析bot、外国企業だけ“黙って”分析をスキップしてた話 — 10-Kと20-Fで「リスクの章立て」が違う罠</title>
      <dc:creator>oji - building AI in public</dc:creator>
      <pubDate>Tue, 08 Sep 2026 23:30:19 +0000</pubDate>
      <link>https://dev.to/masaoshimadaopen/mi-guo-qi-ye-nofen-xi-bot-wai-guo-qi-ye-dakemo-tutefen-xi-wosukitupusitetahua-10-kto20-fderisukunozhang-li-te-gawei-umin-37l3</link>
      <guid>https://dev.to/masaoshimadaopen/mi-guo-qi-ye-nofen-xi-bot-wai-guo-qi-ye-dakemo-tutefen-xi-wosukitupusitetahua-10-kto20-fderisukunozhang-li-te-gawei-umin-37l3</guid>
      <description>&lt;p&gt;どうも、おじいです。平日夜と休日に、AI エージェントとか自動売買 bot をちまちま作ってる 38 歳。&lt;/p&gt;

&lt;p&gt;今日は、副業で開発してる企業分析 bot でやらかした、結構えぐいバグの話。動いてるように見えて、実は大事なデータをごっそり見逃してた。データ処理系の個人開発やってる人には、あるあるかもしれない。&lt;/p&gt;

&lt;h3&gt;
  
  
  何が起きたか
&lt;/h3&gt;

&lt;p&gt;作ってる bot の一つに、米国上場企業の年次報告書（SEC ファイリング）を読み込んで、事業リスクを LLM で要約・評価するやつがある。企業の健全性をざっくり把握するためのツールだ。&lt;/p&gt;

&lt;p&gt;こいつが、ある特定の企業群について、やけに「クリーン」な分析結果を出してくることに気づいた。「リスク要因: 特になし」みたいな。最初は「お、超優良企業か？」なんて思ってたんだけど、何社か続くとさすがにおかしい。そんなわけない。&lt;/p&gt;

&lt;p&gt;ログを掘ってみたら、衝撃の事実が判明した。bot が、特定の企業の分析だけ「黙って」スキップしてた。エラーも吐かずに、ただ空っぽのデータを返してた。そのせいで、後段の LLM は「リスクに関する記述がなかった」と判断して、「リスクなし」という結論を出してたわけだ。これはやばい。&lt;/p&gt;

&lt;h3&gt;
  
  
  原因: 「10-K」と「20-F」という様式の違い
&lt;/h3&gt;

&lt;p&gt;原因は、米国証券取引委員会（SEC）に提出される年次報告書の「様式」の違いにあった。&lt;/p&gt;

&lt;p&gt;bot は当初、米国企業が提出する「&lt;strong&gt;Form 10-K&lt;/strong&gt;」という書類だけを想定して作ってた。この 10-K では、事業リスクは「&lt;strong&gt;Item 1A. Risk Factors&lt;/strong&gt;」という項目に記載されるのがお作法。だから、bot は機械的に「Item 1A」のセクションを引っこ抜くように実装してた。&lt;/p&gt;

&lt;p&gt;ところが、分析結果が空になってた企業を調べたら、全部「外国企業」だった。米国市場に上場してる外国企業は、10-K の代わりに「&lt;strong&gt;Form 20-F&lt;/strong&gt;」という別の様式で報告書を提出する。&lt;/p&gt;

&lt;p&gt;そして、この 20-F では、リスク要因は「&lt;strong&gt;Item 3.D. Risk Factors&lt;/strong&gt;」に書かれている。&lt;/p&gt;

&lt;p&gt;つまり、bot は 20-F の書類に対して、存在しない「Item 1A」を探しに行ってた。当然、見つかるわけがない。&lt;/p&gt;

&lt;p&gt;一番の問題は、その後の処理だった。セクションが見つからなかった時のエラーハンドリングを、こんな風に書いてた。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 抽出失敗と「該当なし」を混同する危険なコード
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;risk_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item 1A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;SectionNotFound&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;risk_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt; &lt;span class="c1"&gt;# これでは失敗したのか、元々空なのか分からない
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;一見、例外をキャッチしてて安全そうに見える。でも、これが罠だった。「セクションが見つからなくて抽出に失敗した」ケースと、「セクションはあったけど中身が空だった」ケースが、どっちも &lt;code&gt;risk_text = ""&lt;/code&gt; になってしまう。&lt;/p&gt;

&lt;p&gt;このせいで、データ欠損という重大な異常が、正常な「リスク記載なし」として処理され、静かにバグが進行してたわけだ。&lt;/p&gt;

&lt;h3&gt;
  
  
  修正: フォームタイプをちゃんと見て、失敗を区別する
&lt;/h3&gt;

&lt;p&gt;対策はシンプル。&lt;br&gt;
まず、処理対象のドキュメントが 10-K なのか 20-F なのかをちゃんと判別する。その上で、フォームタイプに応じて読み込むべきセクション ID を切り替えるようにした。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# フォームタイプに応じて処理を分岐する改善コード
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;form_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10-K&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item 1A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;form_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;20-F&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;section_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item 3.D&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# 未対応のフォームタイプなら、処理を中断
&lt;/span&gt;    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unsupported form type: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;form_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="c1"&gt;# 抽出を試みる。失敗したらNoneが返るようにget_section側を修正
&lt;/span&gt;&lt;span class="n"&gt;risk_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;try_get_section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;section_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;risk_text&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# セクション自体が見つからなかった場合（＝異常）
&lt;/span&gt;    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Section not found: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;section_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ... エラー処理 ...
&lt;/span&gt;&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;risk_text&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# セクションはあったが空だった場合（＝正常）
&lt;/span&gt;    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Risk factors section is empty: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;section_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;get_section&lt;/code&gt; メソッドも修正して、セクションが見つからない場合は空文字じゃなく &lt;code&gt;None&lt;/code&gt; を返すように変更した。&lt;/p&gt;

&lt;p&gt;これで、呼び出し側は戻り値を見るだけで、&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;None&lt;/code&gt; → セクションが見つからなかった（＝想定外のエラー）&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;""&lt;/code&gt; （空文字）→ セクションはあったが、中身が空だった（＝データ上、リスク記載なし）&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(str)&lt;/code&gt; → 正常に抽出できた&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;と、明確に区別できるようになった。この修正後、これまでスキップされてた外国企業の分析も、無事に動き出した。&lt;/p&gt;

&lt;h3&gt;
  
  
  学び: エラーを握りつぶすと、静かに死ぬ
&lt;/h3&gt;

&lt;p&gt;今回の失敗から学んだことは２つ。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ドメイン知識はマジで大事。&lt;/strong&gt; 今回で言えば、SEC ファイリングに 10-K と 20-F の違いがある、という知識。技術的な実装力だけじゃなくて、扱ってるデータそのものへの理解がないと、こういう根本的な見落としをする。特に金融系は、こういう「お作法」の塊みたいな世界。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;「失敗」と「空」を区別する設計。&lt;/strong&gt; エラーを安易に握りつぶして、空文字や &lt;code&gt;0&lt;/code&gt; みたいな「無害そうな」デフォルト値を返すのは、本当に危険。サイレントなデータ欠損は、気づいた時には手遅れ、なんてこともあり得る。自動売買 bot なら、資産を溶かす原因に直結する。&lt;code&gt;None&lt;/code&gt; を使ったり、専用の例外を投げたりして、異常は異常としてちゃんと後続の処理に伝える設計がいかに重要か、身をもって知った。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;副業の個人開発だと、ついドキュメントを斜め読みして「動いたからヨシ！」で進めがちだけど、こういう地味な仕様の確認こそ、一番時間をかけるべき部分なのかもしれない。&lt;/p&gt;

&lt;p&gt;この失敗ログが、同じようにデータ処理系の bot を作ってる誰かの参考になれば嬉しい。&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build and run small Python systems — trading bots, RAG APIs, scheduled automation — and write up whatever breaks along the way.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If a provider-agnostic RAG Q&amp;amp;A API is useful to you, mine is MIT-licensed on GitHub: &lt;a href="https://github.com/masaoshimadaOpen/rag-faq-api" rel="noopener noreferrer"&gt;rag-faq-api&lt;/a&gt;. It runs and passes its full test suite **with no API key&lt;/em&gt;* (offline stub LLM + hashing embedder), swaps to Claude / Gemini / OpenAI via one env var, and ships a retrieval-quality harness (Hit@k / MRR / Recall@k) with a chunking sweep.*&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My Bot's KPI: Trades 2 &gt; Opportunities 1?! When Your Numerator and Denominator Don't Observe the Same Time.</title>
      <dc:creator>oji - building AI in public</dc:creator>
      <pubDate>Fri, 04 Sep 2026 23:30:20 +0000</pubDate>
      <link>https://dev.to/masaoshimadaopen/my-bots-kpi-trades-2-opportunities-1-when-your-numerator-and-denominator-dont-observe-the-4lim</link>
      <guid>https://dev.to/masaoshimadaopen/my-bots-kpi-trades-2-opportunities-1-when-your-numerator-and-denominator-dont-observe-the-4lim</guid>
      <description>&lt;p&gt;Hey everyone, it's your friendly neighborhood "old man" developer here. I'm a 38-year-old part-time engineer, spending my evenings and weekends tinkering with AI bots.&lt;/p&gt;

&lt;p&gt;Today, I want to share a particular moment from my bot development log that had me face-palming – one of those "how could I make such a dumb mistake?" moments. In short, a KPI I built to measure my bot's performance started spitting out logically impossible values. Specifically, my "fulfillment rate" exceeded 100%.&lt;/p&gt;

&lt;p&gt;Imagine a scenario like, "there was only one trading opportunity, but somehow two trades were executed." It made no sense. And no, my bot hadn't gone rogue and started placing infinite orders. The cause was a simple, elementary error in my KPI calculation logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Happened: My Tests Turned Red with an &lt;code&gt;AssertionError&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;I've set up a decent suite of unit and integration tests for my bots. Last weekend, after adding some new logic, I ran the tests as usual, and they went red with an unfamiliar error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AssertionError: fulfillment_rate &amp;gt; 1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fulfillment_rate&lt;/code&gt; is a custom KPI I use to see how many of the trading opportunities my bot identifies actually result in a successful trade. The formula is simple: &lt;code&gt;actual_trades / total_opportunities&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For this to exceed 1.0 (or 100%) is impossible. Digging into the logs, I found bizarre entries like &lt;code&gt;trades: 2&lt;/code&gt; against &lt;code&gt;total_opportunities: 1&lt;/code&gt;. For a moment, I thought there was a bug in my aggregation logic, double-counting trades. But a direct check of the DB confirmed two actual trades. And at that moment, only one opportunity was visible. What was going on...?&lt;/p&gt;

&lt;h3&gt;
  
  
  The Cause: The End of the Month Hadn't Arrived Yet
&lt;/h3&gt;

&lt;p&gt;Stepping through the code with a debugger, the cause became immediately clear. The "observation time" for calculating the numerator and denominator was out of sync.&lt;/p&gt;

&lt;p&gt;The problematic code, conceptually, looked something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Problematic logic (conceptual)
&lt;/span&gt;
&lt;span class="c1"&gt;# Denominator: Fetch all trading opportunities as of the end of the month
&lt;/span&gt;&lt;span class="n"&gt;opportunities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_opportunities_as_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end_of_month_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Numerator: Fetch all trades that occurred within a specific period
&lt;/span&gt;&lt;span class="n"&gt;trades&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_trades_in_period&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Calculate fulfillment rate
&lt;/span&gt;&lt;span class="n"&gt;fulfillment_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trades&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opportunities&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, this code might seem fine. But running it &lt;em&gt;before&lt;/em&gt; the end of the month arrives leads to a nasty problem.&lt;/p&gt;

&lt;p&gt;Let's say I ran the test on August 26th.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Numerator Calculation&lt;/strong&gt;: &lt;code&gt;get_trades_in_period&lt;/code&gt; would retrieve all trades from August 1st to August 26th. If a new trade was entered and executed on August 26th, the numerator &lt;code&gt;len(trades)&lt;/code&gt; would be incremented.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Denominator Calculation&lt;/strong&gt;: &lt;code&gt;get_opportunities_as_of&lt;/code&gt; would try to fetch trading opportunities that &lt;em&gt;should be visible as of &lt;code&gt;end_of_month_date&lt;/code&gt;&lt;/em&gt;, i.e., August 31st. But it's only August 26th. Since future data isn't available yet, the trading opportunities that occurred on August 26th hadn't been included in the denominator calculation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result was a discrepancy: the numerator increased in real-time, while the denominator wouldn't update until the end of the month. If trades were concentrated near month-end, the fulfillment rate would easily exceed 100%. It's obvious in hindsight, but I didn't catch it until the test failed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix: Unifying the Observation Point to the "Entry Date"
&lt;/h3&gt;

&lt;p&gt;Once I pinpointed the cause as a "mismatch in observation time," the fix was straightforward: align the numerator and denominator to be calculated from data observed at the same point in time.&lt;/p&gt;

&lt;p&gt;Specifically, I changed the logic to re-calculate trading opportunities based on the actual "entry date" when the bot made a trade.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Corrected logic (conceptual)
&lt;/span&gt;
&lt;span class="c1"&gt;# Base calculation on the day the trade occurred
&lt;/span&gt;&lt;span class="n"&gt;trade_entry_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_trade_entry_date&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;

&lt;span class="c1"&gt;# Denominator: Get opportunities as of "that specific day" the trade occurred
&lt;/span&gt;&lt;span class="n"&gt;opportunities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_opportunities_as_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trade_entry_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Numerator: Similarly, get trades that occurred "on that specific day"
&lt;/span&gt;&lt;span class="n"&gt;trades&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_trades_on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trade_entry_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Now, the observation points are aligned
&lt;/span&gt;&lt;span class="n"&gt;fulfillment_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trades&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opportunities&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, without waiting for month-end, I can correctly calculate, for each trade, "what percentage of opportunities existing at that specific moment were executed." By leveraging the bot's own business day determination logic, I was able to align the observation points.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Lesson: "Numerator and Denominator on the Same Playing Field" - A Fundamental Principle
&lt;/h3&gt;

&lt;p&gt;This mistake of "misaligned aggregation criteria for numerator and denominator" isn't actually my first time. Since I started personal development, this is probably the fourth time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  User registration counts and active user counts had different aggregation periods, causing active rates to exceed 100%.&lt;/li&gt;
&lt;li&gt;  API total request counts and error counts had different time zones, leading to skewed error rates.&lt;/li&gt;
&lt;li&gt;  In backtesting profit/loss calculations, the timing of fee accrual was off, overstating profits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of them were the same type of mistake.&lt;/p&gt;

&lt;p&gt;"When calculating KPIs, always obtain the numerator and denominator from the same observation point, the same definition, and the same playing field."&lt;/p&gt;

&lt;p&gt;This is a fundamental principle, yet it's easy to overlook when you're deep in the code. It's especially easy to fall into this trap when pulling numbers from different tables or data sources.&lt;/p&gt;

&lt;p&gt;This latest failure was detected by an &lt;code&gt;AssertionError&lt;/code&gt; in my tests, which was great. If I hadn't had tests and remained oblivious, I might have mistakenly thought, "Wow, this bot is performing incredibly well!" and made bad decisions based on that false impression.&lt;/p&gt;

&lt;p&gt;This past weekend was another stark reminder that humble testing and strict adherence to basic principles are paramount. I hope sharing this failure log can be helpful to someone out there.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build and run small Python systems — trading bots, RAG APIs, scheduled automation — and write up whatever breaks along the way.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If a provider-agnostic RAG Q&amp;amp;A API is useful to you, mine is MIT-licensed on GitHub: &lt;a href="https://github.com/masaoshimadaOpen/rag-faq-api" rel="noopener noreferrer"&gt;rag-faq-api&lt;/a&gt;. It runs and passes its full test suite **with no API key&lt;/em&gt;* (offline stub LLM + hashing embedder), swaps to Claude / Gemini / OpenAI via one env var, and ships a retrieval-quality harness (Hit@k / MRR / Recall@k) with a chunking sweep.*&lt;/p&gt;

</description>
      <category>testing</category>
      <category>api</category>
      <category>debugging</category>
      <category>python</category>
    </item>
    <item>
      <title>One Windows Task Scheduler Setting Made the Difference Between Bot Apocalypse and Resiliency</title>
      <dc:creator>oji - building AI in public</dc:creator>
      <pubDate>Thu, 03 Sep 2026 23:30:14 +0000</pubDate>
      <link>https://dev.to/masaoshimadaopen/one-windows-task-scheduler-setting-made-the-difference-between-bot-apocalypse-and-resiliency-20el</link>
      <guid>https://dev.to/masaoshimadaopen/one-windows-task-scheduler-setting-made-the-difference-between-bot-apocalypse-and-resiliency-20el</guid>
      <description>&lt;p&gt;Hey, it's oji_ai_dev here. I'm a 38-year-old developer building AI agents and automated trading bots on the side.&lt;/p&gt;

&lt;p&gt;Today, I want to talk about a super subtle, yet critically important, infrastructure setting. My home server PC crashed overnight, and when I woke up, some of my bots were completely dead. I initially thought it was just a typical PC hiccup, but after digging in, I found it was a silent failure caused by my own human error. These kinds of silent failures are truly brutal.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Happened: My Dashboard Was Too Quiet This Morning
&lt;/h3&gt;

&lt;p&gt;It all started when I woke up and checked my custom monitoring dashboard. Several bot logs, which should have been running, had stopped dead around 3 AM.&lt;/p&gt;

&lt;p&gt;"Ah, the PC crashed again."&lt;/p&gt;

&lt;p&gt;This happens occasionally. Sure enough, the Event Viewer showed an unexpected shutdown record. Probably a power flicker or something. But here's the kicker: I have the PC set to auto-restart, yet out of my 11 running bots, 4 simply hadn't started back up.&lt;/p&gt;

&lt;p&gt;No error logs. They just weren't executing. This is the worst kind of "silent failure." It leads to data loss and missed opportunities. What was the difference between the running bots and the dead ones?&lt;/p&gt;

&lt;h3&gt;
  
  
  The Cause: One Tiny Checkbox
&lt;/h3&gt;

&lt;p&gt;I started comparing the Task Scheduler settings for the 7 bots that restarted successfully and the 4 that remained silent.&lt;/p&gt;

&lt;p&gt;I found it almost immediately. The culprit was this setting in the "Conditions" tab:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Start the task only if the computer is on AC power"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The working bots had this box unchecked, meaning they would start even if the PC was on battery. The 4 dead bots, however, all had this box checked.&lt;/p&gt;

&lt;p&gt;When the PC shut down overnight and restarted, for some fleeting moment, the OS must have detected it as "on battery power." Any tasks scheduled to start at that precise moment were skipped because they weren't on AC power. That was the truth.&lt;/p&gt;

&lt;p&gt;Why did I miss this setting? Tracing back my memory, a few months ago, I had wanted to ensure all bots would reliably restart after a power outage. I went through and updated the settings for all my active bots. I changed 7 of them correctly, but completely forgot the remaining 4. A classic manual rollout mistake. It really drove home how dangerous assumptions can be.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix and Prevention: Auditing Settings with Code
&lt;/h3&gt;

&lt;p&gt;Once I knew the cause, the fix was simple: open the settings for the 4 problematic tasks and uncheck the box.&lt;/p&gt;

&lt;p&gt;But that's not a fundamental solution. I could make the same mistake again. So, I decided to build a system to verify and audit settings using commands.&lt;/p&gt;

&lt;p&gt;First, individual task settings can be exported as XML using the &lt;code&gt;schtasks&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;schtasks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/Query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/TN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MyBotTask"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/XML&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;task.xml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, I could open this XML file and check for &lt;code&gt;&amp;lt;DisallowStartIfOnBatteries&amp;gt;true&amp;lt;/DisallowStartIfOnBatteries&amp;gt;&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;But doing this for all 11 bots is tedious, and I'd surely forget again. This is where PowerShell comes in handy. I wrote a simple script to iterate through all tasks in a specific folder and list any that don't have the desired setting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Detect tasks under the '\MyBots\' path that are configured to NOT start when on battery power (true)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ScheduledTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisallowStartIfOnBatteries&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TaskPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-like&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'\MyBots\*'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running this periodically, or whenever I deploy a new bot, will prevent this specific configuration oversight. Ideally, I'd use something like Ansible for configuration management, but for personal side projects, even a simple auditing script like this is incredibly effective.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson Learned: Manual Configuration Rollouts Always Lead to Accidents
&lt;/h3&gt;

&lt;p&gt;The lesson from this incident is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manual rollouts of configurations will inevitably lead to accidents.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is a given for large-scale systems in my day job, but with personal projects, I get complacent, thinking "only I touch this." However, as the number of bots grows, my memory becomes unreliable. I learned firsthand how dangerous it is to assume "I've done everything."&lt;/p&gt;

&lt;p&gt;System robustness isn't just about fancy algorithms or the latest AI models. It's supported by every single, mundane infrastructure setting. A single checkbox can bring your system's availability to zero.&lt;/p&gt;

&lt;p&gt;Time is limited for side projects. That's why I should have built a system to manage and audit these "set-and-forget" parts of the infrastructure with code from the beginning. Spending half a day troubleshooting is far less productive than spending that time building new features.&lt;/p&gt;

&lt;p&gt;Personal development offers freedom, but it also means you're solely responsible for your infrastructure. This failure was a good opportunity to re-emphasize that responsibility.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build and run small Python systems — trading bots, RAG APIs, scheduled automation — and write up whatever breaks along the way.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If a provider-agnostic RAG Q&amp;amp;A API is useful to you, mine is MIT-licensed on GitHub: &lt;a href="https://github.com/masaoshimadaOpen/rag-faq-api" rel="noopener noreferrer"&gt;rag-faq-api&lt;/a&gt;. It runs and passes its full test suite **with no API key&lt;/em&gt;* (offline stub LLM + hashing embedder), swaps to Claude / Gemini / OpenAI via one env var, and ships a retrieval-quality harness (Hit@k / MRR / Recall@k) with a chunking sweep.*&lt;/p&gt;

</description>
      <category>api</category>
      <category>ai</category>
      <category>devops</category>
      <category>debugging</category>
    </item>
    <item>
      <title>My AI Agent Was Serving Stale Data for 30 Days: A Silent Failure Rooted in State File Freshness</title>
      <dc:creator>oji - building AI in public</dc:creator>
      <pubDate>Wed, 02 Sep 2026 23:30:17 +0000</pubDate>
      <link>https://dev.to/masaoshimadaopen/my-ai-agent-was-serving-stale-data-for-30-days-a-silent-failure-rooted-in-state-file-freshness-4687</link>
      <guid>https://dev.to/masaoshimadaopen/my-ai-agent-was-serving-stale-data-for-30-days-a-silent-failure-rooted-in-state-file-freshness-4687</guid>
      <description>&lt;p&gt;Hey everyone, it's Grandpa Dev. I'm a 38-year-old side-hustle engineer building AI agents and automated trading bots in my evenings and weekends.&lt;/p&gt;

&lt;p&gt;Today, I want to share a pretty nasty silent failure I encountered with one of my AI agents. It took me a full month to notice, and honestly, it was a chilling discovery. I'm documenting this as a failure log in hopes it helps other solo developers running their own automation systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Happened: My Agent Served the Same Content for 30 Days Straight
&lt;/h3&gt;

&lt;p&gt;One of the agents I run is responsible for summarizing market information and generating a daily briefing every morning. Last week, I finally realized it had been serving the exact same content, day after day, for an entire month.&lt;/p&gt;

&lt;p&gt;Specifically, a logic fix I made in late August was never actually reflected. The agent diligently delivered briefings generated with old information and outdated logic from August 28th, for almost the entire month of September. Ouch.&lt;/p&gt;

&lt;p&gt;Digging deeper, I found that a weekly review process was also running with the same old logic. This genuinely sent shivers down my spine. It looked like it was working, but it was effectively dead—a true zombie process.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Investigation: A Compound Failure from Two Factors
&lt;/h3&gt;

&lt;p&gt;After a deep dive into the logs, I finally pinpointed the causes. There were two main issues at play.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Configuration "Drift"
&lt;/h4&gt;

&lt;p&gt;First, at a fundamental level, there was a "drift" where a configuration file change didn't propagate to all the code that referenced it.&lt;/p&gt;

&lt;p&gt;In late August, I modified the path and content of a configuration file to tweak the briefing generation logic. Of course, I updated the main generation code that referenced this setting. But I missed something: I forgot to update another operational script (&lt;code&gt;operator.py&lt;/code&gt;) that indirectly referenced the same configuration file.&lt;/p&gt;

&lt;p&gt;As a result, the new code used the new settings, while the old code continued to look for the old settings. This created a twisted state. The initial misstep was that the design change hadn't fully permeated the entire codebase.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Silent Failure Ignoring State File "Freshness"
&lt;/h4&gt;

&lt;p&gt;However, if it were just configuration drift, I probably would have noticed sooner. The real problem was that the operational code completely ignored the "freshness" of the state file.&lt;/p&gt;

&lt;p&gt;My agent involves several interconnected processes:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;generator.py&lt;/code&gt; creates the raw material for the briefing from the latest information (a state file like &lt;code&gt;staged_brief.json&lt;/code&gt;).&lt;br&gt;
↓&lt;br&gt;
&lt;code&gt;operator.py&lt;/code&gt; reads that state file and performs the final delivery.&lt;/p&gt;

&lt;p&gt;The problematic &lt;code&gt;operator.py&lt;/code&gt;, if &lt;code&gt;staged_brief.json&lt;/code&gt; existed, would read and process it without &lt;em&gt;any&lt;/em&gt; regard for when it was created.&lt;/p&gt;

&lt;p&gt;So, due to the configuration drift, there were days when &lt;code&gt;generator.py&lt;/code&gt; failed and couldn't generate a new &lt;code&gt;staged_brief.json&lt;/code&gt;. But &lt;code&gt;operator.py&lt;/code&gt; didn't care. It found the old &lt;code&gt;staged_brief.json&lt;/code&gt; from the previous day and thought, "Oh, a file exists!" and just reused it, continuing to deliver stale content.&lt;/p&gt;

&lt;p&gt;The logs clearly showed this evidence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// operator.py ignores staged_brief freshness -&amp;gt; FABLE for 8/31 and 9/1 are both "(2026-08-28)" and run_count is identical = 8/28 version was delivered unnoticed for 3 business days.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even when file updates failed, no errors were thrown. Old data was silently used. This was the true nature of the silent failure. Scary stuff.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix: Adding "Freshness Guards" to All State-Handling Code
&lt;/h3&gt;

&lt;p&gt;The fix was simple yet thorough. I added guard mechanisms to every part of the code that reads "time-sensitive data" like state files or caches, checking their modification timestamps.&lt;/p&gt;

&lt;p&gt;Conceptually, the code looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StaleDataError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Custom exception for stale data&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_file_mtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get file modification time as datetime&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromtimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getmtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# --- Revised process ---
&lt;/span&gt;&lt;span class="n"&gt;state_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path/to/staged_brief.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Freshness guard: If the file is older than 1 day, raise an exception to stop processing
&lt;/span&gt;&lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;get_file_mtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state_file&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;StaleDataError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;State file &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state_file&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is older than 1 day. Aborting.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Subsequent processing will now execute with the guarantee that the data is fresh
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;State file is fresh. Proceeding with the operation.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thanks to this guard, if the state file ever stops updating for any reason, the consuming process will now immediately throw a &lt;code&gt;StaleDataError&lt;/code&gt; and terminate, preventing a silent failure.&lt;/p&gt;

&lt;p&gt;Of course, I also addressed the original configuration drift problem by refactoring to unify the configuration file reference paths.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lessons Learned: Fail-Safes Are the Lifeline for Personal Automation
&lt;/h3&gt;

&lt;p&gt;I learned a lot from this failure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Configuration changes are as dangerous as code changes.&lt;/strong&gt; Don't just update documentation or config files and call it a day. Be aware that it affects every corner of the code that references it. You need to be as meticulous as doing a full &lt;code&gt;grep&lt;/code&gt; sweep to check for impacts.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;When dealing with stateful files, always check for "freshness."&lt;/strong&gt; Just checking for file existence isn't enough. Skipping that extra step of verifying the modification time creates a breeding ground for fatal zombie processes like mine.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Robustness is paramount, especially for side projects.&lt;/strong&gt; Unlike a full-time job where you might have 24/7 monitoring, personal projects need fail-safe designs that immediately stop and notify you of errors instead of silently chugging along incorrectly. This is literally a lifeline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;While developing cool new features is fun, building a robust, albeit unsung, system is ultimately the key to sustained personal development. This incident truly drove that home for me.&lt;/p&gt;

&lt;p&gt;I hope this failure log helps someone out there prevent a similar issue in their own projects.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build and run small Python systems — trading bots, RAG APIs, scheduled automation — and write up whatever breaks along the way.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If a provider-agnostic RAG Q&amp;amp;A API is useful to you, mine is MIT-licensed on GitHub: &lt;a href="https://github.com/masaoshimadaOpen/rag-faq-api" rel="noopener noreferrer"&gt;rag-faq-api&lt;/a&gt;. It runs and passes its full test suite **with no API key&lt;/em&gt;* (offline stub LLM + hashing embedder), swaps to Claude / Gemini / OpenAI via one env var, and ships a retrieval-quality harness (Hit@k / MRR / Recall@k) with a chunking sweep.*&lt;/p&gt;

</description>
      <category>ai</category>
      <category>debugging</category>
      <category>python</category>
      <category>automation</category>
    </item>
    <item>
      <title>副業応募botが受注ゼロ！犯人は『毎週クラッシュして成果を捨てる』1行のバグだった話</title>
      <dc:creator>oji - building AI in public</dc:creator>
      <pubDate>Sat, 29 Aug 2026 23:30:17 +0000</pubDate>
      <link>https://dev.to/masaoshimadaopen/fu-ye-ying-mu-botgashou-zhu-zerofan-ren-hamei-zhou-kuratusiyusitecheng-guo-woshe-teru-1xing-nobagudatutahua-2g39</link>
      <guid>https://dev.to/masaoshimadaopen/fu-ye-ying-mu-botgashou-zhu-zerofan-ren-hamei-zhou-kuratusiyusitecheng-guo-woshe-teru-1xing-nobagudatutahua-2g39</guid>
      <description>&lt;p&gt;どうも、おじいです。&lt;/p&gt;

&lt;p&gt;本業の傍ら、AI エージェントとか自動売買 bot を個人開発してる。最近、自分の副業探し自体を自動化しようと思って「副業応募 bot」を作って動かしてた。&lt;/p&gt;

&lt;p&gt;こいつの仕事はシンプル。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; 週末に『ある求人サイト』とか複数のマーケットを巡回して、良さげな案件をリストアップする&lt;/li&gt;
&lt;li&gt; リストアップした案件に対して、俺のスキルセットに合わせた提案文を AI で自動生成して応募する&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;これを1ヶ月ほど動かしてみたんだけど、結果は「受注ゼロ」。まあ、そんなに甘くないよな、と。最初は「提案文の質が悪いのかな」とか「そもそもスキルが足りてないのかも」とか、結構まともな反省をしてた。&lt;/p&gt;

&lt;p&gt;でも、念のためログをちゃんと見てみたら、えぐい事実が発覚した。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;そもそも、提案を1件も送ってなかった。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;いや、マジかよと。630件も提案文を「下書き」は作ってたのに、応募ボタンを押すフェーズまで到達したのがゼロ件。これは提案文の質とかそういうレベルの話じゃない。完全にシステムの問題。マジでやばいと思った。&lt;/p&gt;

&lt;h3&gt;
  
  
  原因：エラー1つで全データを吹き飛ばす脆すぎる設計
&lt;/h3&gt;

&lt;p&gt;なんでこんなことになったのか。原因を深掘りしたら、問題は応募スクリプトじゃなくて、その前段の「案件収集スクリプト」にあった。&lt;/p&gt;

&lt;p&gt;こいつは毎週日曜の深夜に動くように設定してたんだけど、どうやら処理の途中でクラッシュしてたらしい。そして、このクラッシュの仕方が最悪だった。&lt;/p&gt;

&lt;p&gt;当時のコードは、だいたいこんな感じ。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="n"&gt;single&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="n"&gt;crashes&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;whole&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scrape_all&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="c1"&gt;# 複数のサイトをループで巡回
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SITES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# このへんでサイトの仕様変更とかでエラーが出ると…
&lt;/span&gt;        &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# 収集した結果をいったんメモリ上のリストに追加
&lt;/span&gt;            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;scrape_site&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;# エラーが出てもループを止めないように try-except は入れてたつもりだった
&lt;/span&gt;
    &lt;span class="c1"&gt;# ループが全部正常に終わらないと、ここにはたどり着かない
&lt;/span&gt;    &lt;span class="c1"&gt;# つまり、途中でクラッシュすると、それまでの成果は全部消える
&lt;/span&gt;    &lt;span class="nf"&gt;save_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;このコードの問題点は2つ。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;エラー処理の範囲がデカすぎる&lt;/strong&gt;: ループの外側でブラウザを起動してるから、&lt;code&gt;browser.new_page()&lt;/code&gt;みたいな処理で何かあると、&lt;code&gt;try-except&lt;/code&gt;ブロックの外で例外が発生して、スクリプト全体が即死する。&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;最後にしか保存しない&lt;/strong&gt;: 収集した案件データは、全部のサイトを回りきるまでメモリ上の &lt;code&gt;results&lt;/code&gt; リストに溜め込んでる。だから、最後の1サイトでコケたとしても、それまでに集めた全データが失われる。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;実際、ログを追ったら『某マーケット』の HTML 構造が微妙に変わってて、要素が見つからずにタイムアウトエラーを起こしてた。たったこれだけのことで、毎週日曜の深夜、bot は数時間かけて集めたデータを全部ドブに捨てて、黙って死んでたわけだ。そりゃ応募できるわけがない。&lt;/p&gt;

&lt;h3&gt;
  
  
  修正：死ぬことを前提にしたエラー処理と中間保存
&lt;/h3&gt;

&lt;p&gt;この手の長時間動くスクリプトは、「絶対にどこかでコケる」という前提で設計しないとダメだった。反省して、こう書き直した。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;After&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Robust&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="n"&gt;handling&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;intermediate&lt;/span&gt; &lt;span class="n"&gt;saves&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scrape_all&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;all_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="c1"&gt;# サイトごとに、ブラウザの起動から全部やる
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SITES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# エラーが起きる可能性のある処理を全部 try ブロックに入れる
&lt;/span&gt;            &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;site_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;scrape_site&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;all_results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site_results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# 成功したサイトの結果は、その都度ファイルに書き出す
&lt;/span&gt;            &lt;span class="nf"&gt;save_intermediate_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;site_results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# ここでエラーをキャッチすれば、次のサイトの処理には進める
&lt;/span&gt;            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, skipping: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# ちゃんとブラウザを閉じる処理も忘れずに
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;browser&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;locals&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# 最後に全部の結果をマージして保存
&lt;/span&gt;    &lt;span class="nf"&gt;save_final_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ポイントは2つ。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;&lt;code&gt;try-except&lt;/code&gt;をループの内側に入れる&lt;/strong&gt;: これで、あるサイトの処理で失敗しても、そのエラーはループ内で完結する。次のサイトの処理には影響が出ない。&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;中間保存&lt;/strong&gt;: 1サイトのデータが取れたら、その時点ですぐにファイルに保存する。これで、たとえスクリプトが最後の最後でクラッシュしたとしても、それまでに成功した分のデータは手元に残る。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;この修正で、bot はようやく毎週安定して案件データを集めてこれるようになった。&lt;/p&gt;

&lt;h3&gt;
  
  
  学び：本当の課題はコードの先にあった
&lt;/h3&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;p&gt;&lt;strong&gt;「俺がやりたいレベルの副業案件、市場にほとんど出てねえわ」&lt;/strong&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;em&gt;I build and run small Python systems — trading bots, RAG APIs, scheduled automation — and write up whatever breaks along the way.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If a provider-agnostic RAG Q&amp;amp;A API is useful to you, mine is MIT-licensed on GitHub: &lt;a href="https://github.com/masaoshimadaOpen/rag-faq-api" rel="noopener noreferrer"&gt;rag-faq-api&lt;/a&gt;. It runs and passes its full test suite **with no API key&lt;/em&gt;* (offline stub LLM + hashing embedder), swaps to Claude / Gemini / OpenAI via one env var, and ships a retrieval-quality harness (Hit@k / MRR / Recall@k) with a chunking sweep.*&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>python</category>
      <category>automation</category>
    </item>
    <item>
      <title>My monitoring tool lied to me and secretly deleted all past data integrity issues</title>
      <dc:creator>oji - building AI in public</dc:creator>
      <pubDate>Fri, 28 Aug 2026 23:30:18 +0000</pubDate>
      <link>https://dev.to/masaoshimadaopen/my-monitoring-tool-lied-to-me-and-secretly-deleted-all-past-data-integrity-issues-4l71</link>
      <guid>https://dev.to/masaoshimadaopen/my-monitoring-tool-lied-to-me-and-secretly-deleted-all-past-data-integrity-issues-4l71</guid>
      <description>&lt;p&gt;Hey there, it's your resident grandpa coder here. I'm 38 and I spend my evenings and weekends tinkering with AI agents and automated trading bots.&lt;/p&gt;

&lt;p&gt;Today, I want to talk about how my homemade monitoring tool betrayed me. And I mean, a pretty catastrophic betrayal. My watchdog, designed to detect data integrity issues in my bots, quietly switched sides, reported "everything's fine!", and silently wiped out all records of past, unresolved issues.&lt;/p&gt;

&lt;p&gt;These kinds of "silent failures" are genuinely terrifying. By the time you notice, it's often too late. This is a record of how overconfidence in my own code led to my monitoring tool actively concealing the very problems it was supposed to monitor—a truly gnarly self-contradiction.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Discovery: Slack Went Quiet
&lt;/h3&gt;

&lt;p&gt;Each of my bots collects and stores specific data daily. Gaps in this data throw off all downstream analysis and trading logic. So, I had a &lt;code&gt;run_watch&lt;/code&gt; script constantly running, designed to instantly notify me via Slack if any data gaps were detected.&lt;/p&gt;

&lt;p&gt;This script was quite effective. When I first implemented it, I'd frequently get notifications like "Data missing for 2023-10-26!". This helped me quickly catch API changes or my own bugs.&lt;/p&gt;

&lt;p&gt;But then, at some point, the notifications just… stopped.&lt;/p&gt;

&lt;p&gt;"Oh, my bots have been stable lately. Looks like I've fixed all the data integrity issues," I thought, blissfully unaware. I even started to wonder if my coding skills had improved.&lt;/p&gt;

&lt;p&gt;Then one day, I happened to peek directly into the database. And there they were: data gaps, plain as day. Some were from a week ago, others much older, all unresolved. And Slack? Not a peep.&lt;/p&gt;

&lt;p&gt;This was bad. It was like my watchdog wasn't just neglecting its duties, but actively colluding with the burglars.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Cause: Self-Incriminating Logic in the Monitoring Tool
&lt;/h3&gt;

&lt;p&gt;I immediately dug into the &lt;code&gt;run_watch&lt;/code&gt; code. Its logic was roughly like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Scan each bot's data store to get a list of missing dates (&lt;code&gt;missing_dates&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; Load a list of previously notified but unresolved missing data issues, saved in &lt;code&gt;unresolved_issues.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Compare the current scan results (&lt;code&gt;missing_dates&lt;/code&gt;) with the &lt;code&gt;unresolved_issues.json&lt;/code&gt; list.&lt;/li&gt;
&lt;li&gt; If new missing data is found that's not in &lt;code&gt;unresolved_issues.json&lt;/code&gt;, notify Slack and add it to the list.&lt;/li&gt;
&lt;li&gt; Conversely, if an issue is in &lt;code&gt;unresolved_issues.json&lt;/code&gt; but not found in the current scan, assume it's "resolved" and remove it from the list.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The problem lay in logic point #5 and a specific function supporting it.&lt;/p&gt;

&lt;p&gt;Right at the heart of the problem was this &lt;code&gt;_oldest_expected()&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Problematic&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Supposed&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;oldest expected date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;but&lt;/span&gt; &lt;span class="n"&gt;returned&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;oldest currently missing date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_oldest_expected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0000-00-00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Based on its name, this function &lt;em&gt;should&lt;/em&gt; return the oldest date for which a bot is expected to have data. For instance, if a bot started collecting data from 2022-01-01, it should return that date.&lt;/p&gt;

&lt;p&gt;But look at the implementation. All it does is &lt;code&gt;e.get("missing")&lt;/code&gt;, which means it's returning the oldest date from the list of missing data &lt;em&gt;found in the current scan&lt;/em&gt;. If no missing data is found at all, it returns &lt;code&gt;"0000-00-00"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This discrepancy between the name and the implementation created a critical bug.&lt;/p&gt;

&lt;p&gt;If, for some reason, the bot's data source API was temporarily unstable, or the network was flaky, the scan process might return "zero missing items."&lt;/p&gt;

&lt;p&gt;At that moment, &lt;code&gt;run_watch&lt;/code&gt; would interpret it like this:&lt;/p&gt;

&lt;p&gt;"Oh, no missing data found in this scan. That means all those past, unresolved issues stacked up in &lt;code&gt;unresolved_issues.json&lt;/code&gt; must be resolved! Alright, I'll clear them all out!"&lt;/p&gt;

&lt;p&gt;And just like that, records of past data gaps, still very much present in the DB, were completely wiped from the monitoring list. The monitoring tool was actively deleting its own evidence. The tool meant to prevent silent failures was creating the most silent and insidious kind of failure. Irony, indeed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix: When in Doubt, Do Nothing (Fail Safe)
&lt;/h3&gt;

&lt;p&gt;The fix was simple.&lt;/p&gt;

&lt;p&gt;I changed the responsibility of the scan process itself to return information about "how far back it actually scanned." The monitoring tool then uses this "scan range" to determine if an issue is resolved.&lt;/p&gt;

&lt;p&gt;Specifically, each bot's scan function &lt;code&gt;scan()&lt;/code&gt; now returns &lt;code&gt;expected_first&lt;/code&gt; (the earliest date it actually scanned) along with the list of missing data. If the scan range can't be confirmed due to API issues or similar, it returns a future date like &lt;code&gt;"9999-99-99"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The monitoring tool now checks this &lt;code&gt;expected_first&lt;/code&gt;. Only if an unresolved issue date in &lt;code&gt;unresolved_issues.json&lt;/code&gt; is &lt;em&gt;newer&lt;/em&gt; than &lt;code&gt;expected_first&lt;/code&gt; does it consider it "resolved."&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;expected_first&lt;/code&gt; is a future date (meaning the scan range is uncertain), it resolves nothing. It defaults to the safe side. This simple change prevents temporary scan failures from wiping out all historical logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lessons Learned: Don't Over-Trust Your Own Tools
&lt;/h3&gt;

&lt;p&gt;I learned three things from this incident:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Function names must perfectly match their implementation.&lt;/strong&gt; &lt;code&gt;_oldest_missing_found_in_this_scan&lt;/code&gt; would have been better than &lt;code&gt;_oldest_expected&lt;/code&gt;, even if it's a ridiculously long name. When a name lies, you end up deceiving yourself.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;A monitoring tool is itself a single point of failure.&lt;/strong&gt; If it goes silent, everything goes dark. Perhaps I need a meta-monitoring system that considers "no notifications" an anomaly. Something like a DIY Dead Man's Snitch.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Proving a negative is hard.&lt;/strong&gt; A report of "no missing data" needs to distinguish between "there genuinely is no missing data" and "we failed to detect any missing data." A fail-safe design that "does nothing" when uncertain is especially critical for monitoring systems like this.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ultimately, the person you should trust least with the code you write is yourself. This incident was a stark reminder that when things feel "stable lately," that's precisely when you should be most suspicious that some silent time bomb is ticking away.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build and run small Python systems — trading bots, RAG APIs, scheduled automation — and write up whatever breaks along the way.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If a provider-agnostic RAG Q&amp;amp;A API is useful to you, mine is MIT-licensed on GitHub: &lt;a href="https://github.com/masaoshimadaOpen/rag-faq-api" rel="noopener noreferrer"&gt;rag-faq-api&lt;/a&gt;. It runs and passes its full test suite **with no API key&lt;/em&gt;* (offline stub LLM + hashing embedder), swaps to Claude / Gemini / OpenAI via one env var, and ships a retrieval-quality harness (Hit@k / MRR / Recall@k) with a chunking sweep.*&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>debugging</category>
      <category>database</category>
    </item>
    <item>
      <title>My Trading Bot Silently Ignored Signals for 41 Days: The "Opportunity vs. Execution" Logging That Saved Me</title>
      <dc:creator>oji - building AI in public</dc:creator>
      <pubDate>Wed, 26 Aug 2026 23:30:17 +0000</pubDate>
      <link>https://dev.to/masaoshimadaopen/my-trading-bot-silently-ignored-signals-for-41-days-the-opportunity-vs-execution-logging-that-1kgn</link>
      <guid>https://dev.to/masaoshimadaopen/my-trading-bot-silently-ignored-signals-for-41-days-the-opportunity-vs-execution-logging-that-1kgn</guid>
      <description>&lt;p&gt;Hey everyone, it's your resident grandpa-developer here.&lt;/p&gt;

&lt;p&gt;I develop and run AI-powered automated trading bots in my spare time, evenings, and weekends. Recently, one of my production bots went a full 41 days without making a single trade.&lt;/p&gt;

&lt;p&gt;No error logs. My watchdog process monitor was chirping happily. It just looked like it was perpetually "waiting." If the market was quiet and no entry conditions were met, that would be normal. But what if something was broken internally, causing it to ignore signals it &lt;em&gt;should&lt;/em&gt; have acted on?&lt;/p&gt;

&lt;p&gt;This "running but doing nothing" state is the absolute scariest. It's impossible to distinguish normal waiting from a silent failure. It genuinely gave me heart palpitations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Digging into Logs Revealed "Ignored Signals"
&lt;/h3&gt;

&lt;p&gt;41 days of silence felt definitively off, so I decided to pull all the detailed logs and investigate. Usually, I only glance at errors and summaries, but this time, I grep'd through every single debug-level raw log.&lt;/p&gt;

&lt;p&gt;And then, a stark truth emerged.&lt;/p&gt;

&lt;p&gt;Internally, the bot &lt;em&gt;was&lt;/em&gt; generating "signals" — the triggers for trades. Meaning, its logic &lt;em&gt;had&lt;/em&gt; identified moments when it should've thought, "Now's the time, enter!" But in the subsequent logs, there was no record of an order being placed.&lt;/p&gt;

&lt;p&gt;In essence, after generating a signal, the bot was &lt;strong&gt;silently suppressing it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The reason this went undetected for 41 days was a flaw in my log design. I wasn't recording the fact that "a signal occurred" in an easily monitorable way. I was logging "actions" like orders and errors, but not the "opportunities" where an action &lt;em&gt;should&lt;/em&gt; have been taken. This made it impossible to tell if the continuous inaction was normal or abnormal.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix: Log "Opportunities" and "Executions"
&lt;/h3&gt;

&lt;p&gt;To prevent these kinds of silent failures, the only way is to log both what the bot &lt;em&gt;intended to do&lt;/em&gt; and what it &lt;em&gt;actually did&lt;/em&gt;, then monitor the discrepancy. I've dubbed this an "Implementation Fidelity Check."&lt;/p&gt;

&lt;p&gt;Specifically, I modified the log output to count the progression from signal generation to execution across different stages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example of newly instrumented log items
&lt;/span&gt;
&lt;span class="c1"&gt;# For bot A:
# seen:      number of times the monitored event was observed
# qualified: number of times it met signal conditions
# taken:     number of times a position was actually taken
# log -&amp;gt; insider_paper: seen=120 qualified=3 taken=2
&lt;/span&gt;
&lt;span class="c1"&gt;# For bot B:
# fired:    total number of signals generated
# opened:   number of orders actually placed
# rejected: number of orders rejected for any reason
# log -&amp;gt; ny_stack: [SIGNALS] fired=5 opened=5 rejected=[]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this in place, I can immediately spot anomalies like "&lt;code&gt;qualified&lt;/code&gt; is increasing but &lt;code&gt;taken&lt;/code&gt; remains zero" or "the count for &lt;code&gt;fired&lt;/code&gt; and &lt;code&gt;opened&lt;/code&gt; doesn't match."&lt;/p&gt;

&lt;p&gt;I rolled out this system to all my bots and set up daily aggregations. And lo and behold, a host of problems became visible.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bot&lt;/th&gt;
&lt;th&gt;Opportunities&lt;/th&gt;
&lt;th&gt;Executions&lt;/th&gt;
&lt;th&gt;Fulfillment Rate&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;trend_exec&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;🔴 1 opportunity unfulfilled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gotobi_paper&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;67%&lt;/td&gt;
&lt;td&gt;⚠️ Missed end-of-month signals&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For &lt;code&gt;trend_exec&lt;/code&gt;, an API spec change had added a new mandatory parameter to requests, which I hadn't followed up on. Order requests were silently failing every time. 0% fulfillment rate. This was terrible.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;gotobi_paper&lt;/code&gt;, I had a gap in my calendar definition for specific trading days (like 'Gotobi' days at month-end), leading to signals being generated but not executed. 67% fulfillment rate. Another missed opportunity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion: The Scariest Thing in Bot Dev is "Quiet Failure"
&lt;/h3&gt;

&lt;p&gt;What I learned from this incident is that "no errors ≠ normal."&lt;/p&gt;

&lt;p&gt;Failures where a process dies or throws an exception and stops are detectable, so they're manageable. The truly terrifying ones are silent failures that don't throw errors, simply bleeding opportunity in the background.&lt;/p&gt;

&lt;p&gt;To prevent this, a system that records the bot's "intent (opportunities)" separately from its "actions (executions)" and monitors the fulfillment rate is indispensable. Without it, you can't tell the difference between normal waiting and a critical bug.&lt;/p&gt;

&lt;p&gt;In personal dev environments, it's easy to push off setting up monitoring like this. But if you're running bots in production, I've painfully realized it's one of the first features you absolutely &lt;em&gt;must&lt;/em&gt; implement. 41 days of lost opportunities was, frankly, quite painful.&lt;/p&gt;

&lt;p&gt;From now on, when building a new bot, I'll prioritize integrating this "Implementation Fidelity Check" system. If you're building similar bots, I hope this experience proves useful.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build and run small Python systems — trading bots, RAG APIs, scheduled automation — and write up whatever breaks along the way.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If a provider-agnostic RAG Q&amp;amp;A API is useful to you, mine is MIT-licensed on GitHub: &lt;a href="https://github.com/masaoshimadaOpen/rag-faq-api" rel="noopener noreferrer"&gt;rag-faq-api&lt;/a&gt;. It runs and passes its full test suite **with no API key&lt;/em&gt;* (offline stub LLM + hashing embedder), swaps to Claude / Gemini / OpenAI via one env var, and ships a retrieval-quality harness (Hit@k / MRR / Recall@k) with a chunking sweep.*&lt;/p&gt;

</description>
      <category>devops</category>
      <category>debugging</category>
      <category>python</category>
      <category>automation</category>
    </item>
    <item>
      <title>My AI Agent Recommended a Non-Existent Investment Product — Exposing Information Gaps Between Fund Distributors and Asset Manage</title>
      <dc:creator>oji - building AI in public</dc:creator>
      <pubDate>Tue, 25 Aug 2026 23:30:20 +0000</pubDate>
      <link>https://dev.to/masaoshimadaopen/my-ai-agent-recommended-a-non-existent-investment-product-exposing-information-gaps-between-fund-12j5</link>
      <guid>https://dev.to/masaoshimadaopen/my-ai-agent-recommended-a-non-existent-investment-product-exposing-information-gaps-between-fund-12j5</guid>
      <description>&lt;p&gt;Hey everyone, it's your average 38-year-old developer here, hacking away at AI agents and automated trading bots in my evenings and weekends, just like many of you. &lt;/p&gt;

&lt;p&gt;Today, I want to share a recent blunder with a financial information AI agent I built. What started as a chuckle-worthy mistake turned into a serious lesson about "information reliability" — a critical factor that can be fatal for any automated system design.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem: AI Recommends a "Non-Existent Investment Trust"
&lt;/h3&gt;

&lt;p&gt;With the new NISA (Japan's tax-advantaged investment program) kicking off, I was building an AI agent to list investment trusts tracking specific indices, primarily to rebalance my own portfolio. I started by asking it to compare products linked to the trending "FANG+" index.&lt;/p&gt;

&lt;p&gt;Within seconds of my prompt, the AI confidently replied:&lt;/p&gt;

&lt;p&gt;"For FANG+ linked investment trusts, considering trust fees and performance, I recommend the following two:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;eMAXIS Slim FANG+&lt;/strong&gt;: This series aims for the lowest operational costs in the industry and is highly recommended.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;iFreeNEXT FANG+&lt;/strong&gt;: It has a strong track record and offers stable management."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Hmm, interesting. I knew about iFreeNEXT FANG+, but "eMAXIS Slim FANG+"?&lt;/p&gt;

&lt;p&gt;"Wait, is that even a thing?"&lt;/p&gt;

&lt;p&gt;I know the eMAXIS Slim series is popular for things like S&amp;amp;P500 and global index funds, but FANG+? I'd never heard of it. Skeptical, I quickly Googled it.&lt;/p&gt;

&lt;p&gt;...And indeed, it didn't exist.&lt;/p&gt;

&lt;p&gt;The AI had confidently fabricated a plausible-sounding product and recommended it. This was a red flag.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deeper Dive: The Trap of Information Hierarchy
&lt;/h3&gt;

&lt;p&gt;AI "hallucinations" are common. It likely combined the popular "eMAXIS Slim" brand with the popular "FANG+" index to generate a seemingly plausible answer. &lt;/p&gt;

&lt;p&gt;It would be easy to just dismiss AI as "unreliable" and move on. But as an engineer, I wanted to dig deeper. What if the agent had only returned real product names? Would I have blindly trusted it and moved on to the next step?&lt;/p&gt;

&lt;p&gt;Curious, I decided to cross-reference multiple sources for the legitimate "iFreeNEXT FANG+". Specifically, I checked the website of "Company A" (a distributor selling the product) and the official website of "Daiwa Asset Management" (the asset manager that operates the fund).&lt;/p&gt;

&lt;p&gt;What I found was even more concerning:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The NISA growth investment category eligibility status conflicted between the two sources.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Asset Manager's (Daiwa Asset) Official Site&lt;/strong&gt;: Clearly stated as eligible.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Distributor's (Company A) Site&lt;/strong&gt;: Seemed to indicate it was ineligible, or the information was outdated and not updated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which one is correct? Unsurprisingly, it's the "asset manager" who creates the product. The distributor is essentially a "retailer" that procures and sells the product. They might have delayed updates or simple transcription errors.&lt;/p&gt;

&lt;p&gt;This incident made me realize that information has a clear "hierarchy":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Primary Information&lt;/strong&gt;: The source of the information, like the asset manager's official website.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Secondary Information&lt;/strong&gt;: Information that processes or reposts primary information, such as distributor websites, news articles, or blogs.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;AI Generation&lt;/strong&gt;: Information learned, re-synthesized, and generated by AI from these sources.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My agent had completely ignored this hierarchy. It treated all information gathered from the internet as flat data, simply summarized by the AI. This inherent risk meant it could recommend non-existent products or be misled by outdated secondary data.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix: Embedding Fact-Checking into Code
&lt;/h3&gt;

&lt;p&gt;This incident fundamentally changed my approach to designing information gathering agents. It's not enough to just have AI "research"; you need to build in mechanisms to "verify" for it to be practical.&lt;/p&gt;

&lt;p&gt;The solution was simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Designate Reliable Sources as "Truth"&lt;/strong&gt;: In this case, data obtained from the "asset manager's official website" is defined as the master data (primary information).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Automate Cross-Checking&lt;/strong&gt;: Any data obtained from AI or other secondary sources &lt;em&gt;must&lt;/em&gt; be cross-referenced against the master data for fact-checking.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Specifically, I implemented logic like this using Python (pandas):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="c1"&gt;# Primary data obtained from the asset manager's official website (master data)
&lt;/span&gt;&lt;span class="n"&gt;primary_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Fund Name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;iFreeNEXT FANG+&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;iFreeNEXT NASDAQ100&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Asset Manager&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Daiwa Asset Management&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Daiwa Asset Management&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;NISA Growth Eligible&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Source&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Asset Manager Official&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Asset Manager Official&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;df_primary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;primary_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# AI's recommendation list (including the fabricated product)
&lt;/span&gt;&lt;span class="n"&gt;ai_recommendation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Fund Name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;eMAXIS Slim FANG+&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;iFreeNEXT FANG+&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Reason&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Lowest trust fees&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Strong track record&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;df_ai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ai_recommendation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="c1"&gt;# --- Fact-checking logic ---
# 1. Check if AI's recommendation exists in primary data (master)
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fund_name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;df_ai&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Fund Name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fund_name&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;df_primary&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Fund Name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[WARNING] AI recommended &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fund_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; which does not exist in master data.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Merge distributor information with primary information to detect discrepancies
# (Skipped here, but involves merging distributor data with df_primary and checking for differences)
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code doesn't blindly trust the AI's output. It first checks if the fund names recommended by the AI exist in our defined &lt;code&gt;df_primary&lt;/code&gt; (primary information) list. Anything not found is flagged as a "warning".&lt;/p&gt;

&lt;p&gt;Furthermore, by merging specifications from secondary sources (like distributor sites) with the primary information, we can automatically detect issues like the "NISA eligibility discrepancy."&lt;/p&gt;

&lt;h3&gt;
  
  
  The Lesson: Master Your Primary Sources
&lt;/h3&gt;

&lt;p&gt;What I learned from this failure is that AI is a highly capable assistant, but not the ultimate decision-maker. Especially in domains requiring accuracy, such as finance or technical information, a system to verify AI output is the lifeline.&lt;/p&gt;

&lt;p&gt;And the foundation of that verification is the ability to discern "which information is primary."&lt;/p&gt;

&lt;p&gt;When building automated systems, it's easy to gravitate towards readily accessible secondary information or APIs. But we must constantly ask: where did this information come from? Is its "freshness" and "reliability" guaranteed?&lt;/p&gt;

&lt;p&gt;Ultimately, the biggest leverage comes from automating the tedious process of verification. Even if the AI lies, the overall system can still produce correct outputs. This incident reaffirmed my commitment to building robust systems that can handle such challenges. ✍️&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>automation</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>When I Asked AI to Analyze "FANG+" and It Started Investigating Oil Companies — The Ticker Symbol Trap</title>
      <dc:creator>oji - building AI in public</dc:creator>
      <pubDate>Tue, 25 Aug 2026 03:26:48 +0000</pubDate>
      <link>https://dev.to/masaoshimadaopen/when-i-asked-ai-to-analyze-fang-and-it-started-investigating-oil-companies-the-ticker-symbol-2la7</link>
      <guid>https://dev.to/masaoshimadaopen/when-i-asked-ai-to-analyze-fang-and-it-started-investigating-oil-companies-the-ticker-symbol-2la7</guid>
      <description>&lt;p&gt;Hey there, it's your friendly neighborhood old guy. By day, I'm doing my regular gig, but by night, I'm tinkering with AI agents and algo-trading bots – I'm 38, for context.&lt;/p&gt;

&lt;p&gt;Last weekend, while trying to integrate a new strategy into my investment analysis bot, I stumbled into a rather amusing (and frankly, a bit dangerous) pitfall. Consider this a self-admonition, meticulously documented.&lt;/p&gt;

&lt;h3&gt;
  
  
  Asked for FANG+ Analysis, Got an Oil Price Report
&lt;/h3&gt;

&lt;p&gt;The genesis of the problem was simple. I was curious about recent tech stock movements, so I instructed my custom AI agent: "Analyze the FANG+ index and report on its future outlook."&lt;/p&gt;

&lt;p&gt;This agent is designed to gather relevant data based on a given theme, analyze it, and generate a summary. As usual, I kicked off the task, and it started processing immediately.&lt;/p&gt;

&lt;p&gt;Minutes later, I looked at the generated report and scratched my head.&lt;/p&gt;

&lt;p&gt;"...Something's off, isn't it?"&lt;/p&gt;

&lt;p&gt;The report indeed stated "FANG analysis." But the content had absolutely no mention of tech companies. Instead, it was all about the energy sector: crude oil price trends, shale oil extraction costs, OPEC production volumes.&lt;/p&gt;

&lt;p&gt;It felt like reading an earnings call transcript for an oil company.&lt;/p&gt;

&lt;p&gt;"Wait, why? Did I mess up the prompt?"&lt;/p&gt;

&lt;p&gt;No, I double-checked the logs; the instruction was clearly "Analyze FANG+." Did the AI bug out? I wondered, but tracing its thought process revealed that the AI itself had acted with extreme logic. The problem lay in the "environment" I had provided.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Culprit: "FANG" Lurking in the S&amp;amp;P 500 List
&lt;/h3&gt;

&lt;p&gt;I quickly pinpointed the cause.&lt;/p&gt;

&lt;p&gt;When this AI agent searches for analysis targets, it refers to an investment universe (a list of target stocks) that I've prepped. In this case, I had fed it an S&amp;amp;P 500 constituent list I had on hand, verbatim.&lt;/p&gt;

&lt;p&gt;Here’s an excerpt from that list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;VRSK&lt;/span&gt; &lt;span class="n"&gt;XEL&lt;/span&gt; &lt;span class="n"&gt;CTSH&lt;/span&gt; &lt;span class="n"&gt;TTWO&lt;/span&gt; &lt;span class="n"&gt;LULU&lt;/span&gt; &lt;span class="n"&gt;FANG&lt;/span&gt; &lt;span class="n"&gt;CEG&lt;/span&gt; &lt;span class="n"&gt;TEAM&lt;/span&gt; &lt;span class="n"&gt;AZN&lt;/span&gt; &lt;span class="n"&gt;ZS&lt;/span&gt; &lt;span class="n"&gt;DXCM&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You probably see it now.&lt;/p&gt;

&lt;p&gt;When the AI received the instruction "FANG+", it found the string "FANG" within this list. It then interpreted this as "This must be the FANG the user is referring to," and commenced its analysis.&lt;/p&gt;

&lt;p&gt;This ticker symbol, &lt;code&gt;FANG&lt;/code&gt;, has, of course, absolutely nothing to do with the tech index. It's the ticker for "Diamondback Energy, Inc.," an oil and natural gas company based in Texas.&lt;/p&gt;

&lt;p&gt;No wonder I got a report on crude oil prices. The AI wasn't wrong. If anything, it made the most "logical" decision given the dataset. This misunderstanding, frankly, was wild. A human would infer "contextually, they mean the tech index," but AI purely deals with data. Its purity, in this case, backfired.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix: Eliminate Ambiguity, Define Universe Strictly
&lt;/h3&gt;

&lt;p&gt;To prevent these kinds of incidents, there's only one way: rigorously eliminate ambiguity from the instructions and data given to the AI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Be Specific with Prompts&lt;/strong&gt;&lt;br&gt;
A vague instruction like "Analyze FANG+" was the problem. If I had specified the ticker symbol, such as "Analyze the NYSE FANG+ Index (^NYFANG)", there would have been no room for the AI to err.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Separate Data Universes&lt;/strong&gt;&lt;br&gt;
Fundamentally, treating individual stocks (Stock) and indices (Index) or ETFs within the same list was a mistake.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data source for stocks&lt;/li&gt;
&lt;li&gt;Data source for ETFs&lt;/li&gt;
&lt;li&gt;Data source for indices
These should have been clearly separated, and the AI agent's search scope should have been limited. For example, if the task is "index analysis," it should be constrained to only refer to the index list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For now, I've revised the prompt template and added a UI where the user (me) explicitly specifies the type of analysis target (stock, ETF, index). This should significantly reduce the risk of ticker symbol collisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Lesson: Financial Data is a Minefield of "Name Collisions"
&lt;/h3&gt;

&lt;p&gt;The lesson from this failure is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In the world of financial data, similar strings frequently mean entirely different things.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Just as "FANG" is a colloquial term for a tech index and simultaneously the ticker for an oil company, these kinds of "name collisions" are everywhere. For example, &lt;code&gt;AMZN&lt;/code&gt; is Amazon, but &lt;code&gt;AMZ&lt;/code&gt; might be a JPMorgan index-linked security.&lt;/p&gt;

&lt;p&gt;You cannot expect AI to perform the "contextual reading" that humans do unconsciously. When developing AI agents, the "Garbage In, Garbage Out" principle is absolute. No matter how advanced the LLM, if the reference data sources or underlying assumptions are flawed, the output will be unreliable.&lt;/p&gt;

&lt;p&gt;Squashing these mundane bugs one by one, that's the reality of solo dev work, I think. More than flashy new features, this foundational data hygiene is far more crucial for a bot's stable operation.&lt;/p&gt;

&lt;p&gt;Now, which bug to squash next? I'll report back if I mess up again. 👍&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>automation</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Predicting Shareholder Meeting Failure: How AI Quantified Risk from a Single Sentence in Past Minutes and a 5x Increase in Share</title>
      <dc:creator>oji - building AI in public</dc:creator>
      <pubDate>Mon, 24 Aug 2026 00:28:11 +0000</pubDate>
      <link>https://dev.to/masaoshimadaopen/predicting-shareholder-meeting-failure-how-ai-quantified-risk-from-a-single-sentence-in-past-4265</link>
      <guid>https://dev.to/masaoshimadaopen/predicting-shareholder-meeting-failure-how-ai-quantified-risk-from-a-single-sentence-in-past-4265</guid>
      <description>&lt;p&gt;Hey, it's your friendly neighborhood dev-grandpa here, still chugging along building AI agents on weeknights and weekends.&lt;/p&gt;

&lt;p&gt;Today, I want to share a story about how one of my custom analysis bots dug up a significant risk from a completely unexpected angle. I'll walk through how the AI quantitatively assessed the probability of a seemingly unpredictable event—whether a shareholder meeting would successfully pass a critical resolution—by analyzing past meeting minutes and other disclosure documents.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Trigger: Will This Reverse Stock Split Actually Pass?
&lt;/h3&gt;

&lt;p&gt;I had a particular U.S. company under monitoring by my bot. This company was planning a reverse stock split, a common move to boost share price, usually approved without issue at shareholder meetings.&lt;/p&gt;

&lt;p&gt;But something felt off. This company had become incredibly popular with retail investors, and its shareholder base was growing rapidly. I started to wonder: if the shareholders were too dispersed, would it be difficult to reach the required quorum for the meeting to proceed?&lt;/p&gt;

&lt;p&gt;To test this hypothesis, I instructed my AI agent: "Read all of this company's past SEC filings (like EDINET in Japan) and identify any risks related to shareholder meetings."&lt;/p&gt;

&lt;h3&gt;
  
  
  The AI's Discovery: A Single Sentence Revealing Past Failures
&lt;/h3&gt;

&lt;p&gt;The agent accessed the EDGAR database and started downloading years' worth of proxy statements (DEF 14A/C). Hundreds of megabytes. A human would spend days just reading through it.&lt;/p&gt;

&lt;p&gt;Using NLP, it began parsing the text, extracting relevant passages based on keywords suggesting meeting delays or failures, like "quorum," "adjourn," and "postpone."&lt;/p&gt;

&lt;p&gt;A few minutes later, the agent pinged me on Slack:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Found a history of the Special Meeting being adjourned twice due to a lack of a quorum several years ago."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No way. Seriously?&lt;/p&gt;

&lt;p&gt;The discovery was a single sentence buried in the minutes from that time:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;... the Special Meeting was adjourned on two occasions due to a lack of a quorum. At the reconvened meeting, approximately 45.9% of the outstanding shares were present...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In essence: "The meeting was postponed twice because they couldn't get a quorum. Even when reconvened, only 45.9% of shares were present." That's below the majority. This is a big deal; usually, this doesn't happen.&lt;/p&gt;

&lt;p&gt;This qualitative information – the fact that they'd screwed up before – was already a significant red flag.&lt;/p&gt;

&lt;h3&gt;
  
  
  Combining Qualitative Insights with Quantitative Data
&lt;/h3&gt;

&lt;p&gt;But the AI's job wasn't done. The mere fact that they "failed in the past" is too crude for proper analysis. The crucial next step was to compare: &lt;strong&gt;"How different are the conditions now compared to then?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So, I gave the agent the next set of tasks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Retrieve the number of outstanding shares at the time of the quorum failure.&lt;/li&gt;
&lt;li&gt; Retrieve the current number of outstanding shares.&lt;/li&gt;
&lt;li&gt; Compare the two to estimate the degree of shareholder dispersion.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The results came back quickly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Outstanding shares then: approx. 180 million&lt;/li&gt;
&lt;li&gt;  Outstanding shares now: approx. 950 million&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Astoundingly, since the failed shareholder meeting, the number of outstanding shares had ballooned by &lt;strong&gt;5.1 times&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is where the dots finally connected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Qualitative (Past Information):&lt;/strong&gt; This company inherently struggles with proxy solicitation for shareholder meetings.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Quantitative (Current Data):&lt;/strong&gt; The number of shareholders has increased by 5.1 times since then, making further dispersion extremely likely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combining these two pieces, I could form a highly accurate hypothesis: "The risk of this upcoming shareholder meeting failing due to lack of a quorum is considerably high."&lt;/p&gt;

&lt;p&gt;In code, the logic looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_governance_risks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Extracts governance risks from disclosure document text&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;risks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="c1"&gt;# History of adjournment due to lack of quorum
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;adjourned .* due to a lack of a quorum&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;risks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;QUORUM_FAILURE_HISTORY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

    &lt;span class="c1"&gt;# Extract attendance rate
&lt;/span&gt;    &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;(\d+\.\d+)% of the outstanding shares were present&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;attendance_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attendance_rate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;50.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;risks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LOW_ATTENDANCE_RATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attendance_rate&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;risks&lt;/span&gt;

&lt;span class="c1"&gt;# --- Evaluation Logic ---
# Past document text (sample)
&lt;/span&gt;&lt;span class="n"&gt;past_document_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;...&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;Special&lt;/span&gt; &lt;span class="n"&gt;Meeting&lt;/span&gt; &lt;span class="n"&gt;was&lt;/span&gt; &lt;span class="n"&gt;adjourned&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;two&lt;/span&gt; &lt;span class="n"&gt;occasions&lt;/span&gt; &lt;span class="n"&gt;due&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;lack&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;quorum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;At&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;reconvened&lt;/span&gt; &lt;span class="n"&gt;meeting&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;approximately&lt;/span&gt; &lt;span class="mf"&gt;45.9&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;outstanding&lt;/span&gt; &lt;span class="n"&gt;shares&lt;/span&gt; &lt;span class="n"&gt;were&lt;/span&gt; &lt;span class="n"&gt;present&lt;/span&gt;&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="c1"&gt;# Past and current outstanding shares (in millions)
&lt;/span&gt;&lt;span class="n"&gt;shares_past&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;186.8&lt;/span&gt;
&lt;span class="n"&gt;shares_current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;949.7&lt;/span&gt;

&lt;span class="n"&gt;risks_found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_governance_risks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;past_document_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;share_increase_factor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shares_current&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;shares_past&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;risks_found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;QUORUM_FAILURE_HISTORY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;share_increase_factor&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;High Risk: History of quorum issues AND shareholder base dispersed by &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;share_increase_factor&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;x.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Final Piece: Reading the Company's Countermeasures
&lt;/h3&gt;

&lt;p&gt;However, it's still too early to conclude "high risk!" The company must have learned from its past mistakes.&lt;/p&gt;

&lt;p&gt;I had the agent re-read the latest proxy statement in detail. Sure enough, they had taken action.&lt;/p&gt;

&lt;p&gt;Among the proposals for the current meeting was an agenda item to "amend the Bylaws to reduce the quorum for shareholder meetings from a majority to one-third."&lt;/p&gt;

&lt;p&gt;Smart move. They were lowering the bar themselves.&lt;/p&gt;

&lt;p&gt;This significantly offsets the risk of a quorum failure. Still, the facts of past failures and the rapid increase in shareholders remain. The final assessment landed somewhere around: "Risk still exists, but it's controlled to a non-fatal level."&lt;/p&gt;

&lt;h3&gt;
  
  
  Lessons Learned: Combining Text and Numbers Changes Everything
&lt;/h3&gt;

&lt;p&gt;This whole experience offered significant lessons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Combining qualitative information (text) with quantitative data (numbers) explodes the resolution of analysis.&lt;/strong&gt; AI agents excel at processing both at high speed.&lt;/li&gt;
&lt;li&gt;  Even for seemingly unpredictable events, digging into past logs (meeting minutes, error logs, anything) allows for some degree of quantitative risk prediction for the future.&lt;/li&gt;
&lt;li&gt;  Ignoring countermeasures or changes made by the other party (company, system, market) leads to incomplete analysis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't just about automated trading. It applies to predicting failures in your own services, measuring the effectiveness of marketing campaigns, and many other scenarios. &lt;/p&gt;

&lt;p&gt;Ultimately, AI isn't a magic wand. It's a tool for automating the grunt work of diligent data collection and then interpreting that data to form hypotheses. But because of it, you can sometimes uncover insights that a human would never find. That's why this side hustle is so engaging.&lt;/p&gt;

&lt;p&gt;Alright, which bot should I tinker with next? I'll share if I stumble upon any more interesting failure logs. See ya.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>automation</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>My AI Lied to Me About a Stock Crash — Adversarial Testing Revealed Its Limits (and How to Use AI Correctly)</title>
      <dc:creator>oji - building AI in public</dc:creator>
      <pubDate>Sat, 22 Aug 2026 23:30:17 +0000</pubDate>
      <link>https://dev.to/masaoshimadaopen/my-ai-lied-to-me-about-a-stock-crash-adversarial-testing-revealed-its-limits-and-how-to-use-ai-31al</link>
      <guid>https://dev.to/masaoshimadaopen/my-ai-lied-to-me-about-a-stock-crash-adversarial-testing-revealed-its-limits-and-how-to-use-ai-31al</guid>
      <description>&lt;p&gt;Hey, it's OJ. I'm 38 and dabble in AI agents and algorithmic trading bots as a side gig.&lt;/p&gt;

&lt;p&gt;Recently, I had a pretty "brutal" experience: an AI agent I built straight-up lied to me. As part of my build-in-public journey, I felt this was a crucial lesson to share, especially for anyone trying to automate information gathering with AI. So, here's the log.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Trigger: A Stock I Held Dropped -33%
&lt;/h3&gt;

&lt;p&gt;One day, a stock I was watching plummeted -33%. Normally, I'd immediately jump to official disclosure sites or the company's IR page, frantically searching for timely disclosures.&lt;/p&gt;

&lt;p&gt;But this time, I saw it as a perfect opportunity to test a research AI agent I was developing. Its supposed job: give it a company name and a time period, and it would collect related news and disclosures from the web, then analyze and report on stock price movement factors.&lt;/p&gt;

&lt;p&gt;So, I threw it a prompt: "Report the reason for XX company's sudden stock drop, citing specific sources." Honestly, I expected a faster, more accurate answer than I could get manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  The AI's "Plausible" Analysis Report
&lt;/h3&gt;

&lt;p&gt;A few minutes later, the AI generated a report that &lt;em&gt;looked&lt;/em&gt; legitimate. Its conclusion:&lt;/p&gt;

&lt;p&gt;"The stock price drop is not due to a company-specific issue, but rather a sector-wide risk-off event."&lt;/p&gt;

&lt;p&gt;It even provided supporting evidence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;AI's&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;plausible&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;but&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;incorrect&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;explanation&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ai_response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"conclusion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The drawdown is sector-wide, NOT company-specific."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Peers like Company A (-10.7%) and Company B (-14.1%) also dropped on the same day."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reasoning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"It is unlikely that a Japan-specific catalyst would cause a US-based peer to fall 10.7%."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It cited Company A falling -10.7% and Company B falling -14.1% on the same day, arguing that "it's unlikely a Japan-specific catalyst would cause a US-based peer to drop over 10%." On the surface, it made sense.&lt;/p&gt;

&lt;p&gt;For a moment, I almost accepted it: "Ah, right, the market sentiment must have been bad." But -33%? That's an extreme drop. It was too abnormal to simply dismiss as a sector-wide issue.&lt;/p&gt;

&lt;p&gt;This gut feeling was what allowed me to uncover the AI's lie.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Answer Was in the Primary Source
&lt;/h3&gt;

&lt;p&gt;In the end, I did what I always do: I dug into the primary sources myself. I went to the company's IR site, and the answer was immediately there.&lt;/p&gt;

&lt;p&gt;"Notice Regarding Issuance of New Shares Through Third-Party Allotment."&lt;/p&gt;

&lt;p&gt;It was a public offering. And the dilution rate was a significant 15.79%. This was announced &lt;em&gt;after&lt;/em&gt; market close. No wonder the stock almost hit its limit down the next day. It was entirely a company-specific factor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ground&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;truth&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;found&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;primary&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;sources&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ground_truth&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"catalyst"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Public stock offering announced after market close."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"impact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"15.79% dilution of shares."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Stock price dropped -15.02% the next day (limit down)."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI completely ignored this crucial primary information (the PDF disclosure) and instead picked up only secondary data (other companies' stock prices) from web searches, fabricating a plausible narrative.&lt;/p&gt;

&lt;p&gt;AI doesn't say "I don't know." It fabricates the most coherent story from the fragmented information it has. This was my personal experience with hallucination.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Limits of AI, and How to Use It Correctly
&lt;/h3&gt;

&lt;p&gt;This incident drastically changed my perspective on AI.&lt;/p&gt;

&lt;p&gt;In other cases, the AI &lt;em&gt;had&lt;/em&gt; correctly identified reasons for stock price changes. But even then, the evidence it presented was always news articles reported &lt;em&gt;after&lt;/em&gt; the stock had moved. In other words, AI can offer post-hoc explanations, but it cannot predict. Obvious, but a critical distinction.&lt;/p&gt;

&lt;p&gt;Blindly relying on AI for "answers" is too dangerous. So, how &lt;em&gt;should&lt;/em&gt; we use it?&lt;/p&gt;

&lt;p&gt;My conclusion: &lt;strong&gt;use it as an "aid" for human primary source research.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The role of my AI agent now isn't to "give answers." It's to "accelerate the process of finding answers."&lt;/p&gt;

&lt;p&gt;For example, I use it like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"From this 220,000-character financial report PDF, extract all mentions of 'impairment loss' and 'goodwill,' then summarize them chronologically."&lt;/li&gt;
&lt;li&gt;"From 5 years of timely disclosure data, list the dates and overviews of any announcements regarding 'new share issuance' or 'stock splits.'"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tasks like extracting keywords and organizing vast amounts of data, which would take a human several hours, are delegated to the AI. Based on the output, I perform the final analysis and make judgments myself. The AI is merely an excellent research assistant. I'm the driver.&lt;/p&gt;

&lt;p&gt;Never take AI-generated text at face value; always verify with primary sources. Skip this crucial step, and you'll eventually be tripped up by a "plausible lie" like I was.&lt;/p&gt;

&lt;p&gt;As someone who develops AI myself, I need to understand its limitations better than anyone. This failure was a valuable lesson that reinforced that understanding.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>automation</category>
      <category>buildinpublic</category>
    </item>
  </channel>
</rss>
