<?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: Hidenari Fujiwara</title>
    <description>The latest articles on DEV Community by Hidenari Fujiwara (@hidenari).</description>
    <link>https://dev.to/hidenari</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%2F4077248%2F594b9fc2-5c51-4a9b-a400-929fa7376e88.png</url>
      <title>DEV Community: Hidenari Fujiwara</title>
      <link>https://dev.to/hidenari</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hidenari"/>
    <language>en</language>
    <item>
      <title>Your Browser Automation Clicks Might Be Landing 25% Off — And Nothing Will Tell You</title>
      <dc:creator>Hidenari Fujiwara</dc:creator>
      <pubDate>Sun, 16 Aug 2026 01:52:34 +0000</pubDate>
      <link>https://dev.to/hidenari/your-browser-automation-clicks-might-be-landing-25-off-and-nothing-will-tell-you-2da</link>
      <guid>https://dev.to/hidenari/your-browser-automation-clicks-might-be-landing-25-off-and-nothing-will-tell-you-2da</guid>
      <description>&lt;p&gt;I spent a day pointing an AI agent at a browser to publish one product across four marketplaces. Most of it worked. The parts that didn't work failed in the worst possible way: &lt;strong&gt;silently&lt;/strong&gt;, with no error, no exception, and no log line.&lt;/p&gt;

&lt;p&gt;Here are the five failure modes I hit, in the order I hit them, and the probe that turned the worst one from "this site is broken" into a two-line fix.&lt;/p&gt;

&lt;p&gt;Everything below is from one working session. No hypotheticals.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom: clicks that do nothing
&lt;/h2&gt;

&lt;p&gt;I asked the agent to click a menu item. The tool reported success:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[computer:left_click] Clicked at (383, 734)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing happened. No menu opened, no network request fired, no console error. I re-read the DOM — the element was there, visible, not disabled, not covered by an overlay:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elementFromPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;383&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;734&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; &amp;lt;span&amp;gt;削除&amp;lt;/span&amp;gt;   ← the exact element I wanted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the coordinate was right, the element was right, and the click "succeeded". And nothing happened.&lt;/p&gt;

&lt;p&gt;I burned close to an hour assuming the site was blocking synthetic input. It wasn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The probe: make the page answer for itself
&lt;/h2&gt;

&lt;p&gt;Instead of reasoning about it, I made the page report whether a click ever arrived. Inject a button at a known position with a counter:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;__clicktest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CLICK TEST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cssText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;position:fixed;top:200px;left:200px;width:220px;height:60px;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;z-index:2147483647;background:#f0f;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__clickOK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__clickOK&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="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__trusted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isTrusted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__clickOK&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="s2"&gt;`center=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; center=310,230&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then click that exact center and read the counter:&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__clickOK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// =&amp;gt; 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero. The click was not landing on a button that occupied 220×60 pixels at a coordinate I had just measured. That rules out "the site blocks synthetic events" — a blocked event still &lt;em&gt;arrives&lt;/em&gt;; it just gets ignored by the handler. Mine never arrived.&lt;/p&gt;

&lt;p&gt;So the coordinate space was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The math: two coordinate spaces, one number
&lt;/h2&gt;

&lt;p&gt;The automation tool takes coordinates in &lt;strong&gt;screenshot space&lt;/strong&gt;. The page reports coordinates in &lt;strong&gt;CSS pixel space&lt;/strong&gt;. If the screenshot is captured at a different scale than the viewport, every coordinate you read from the DOM is wrong by a constant factor — and the failure is invisible, because a click still happens, just somewhere else.&lt;/p&gt;

&lt;p&gt;I measured the factor by bisecting. Device-pixel space (&lt;code&gt;×2&lt;/code&gt;, since &lt;code&gt;devicePixelRatio&lt;/code&gt; was 2) missed. Then:&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__clickOK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// after clicking (232, 172) instead of (310, 230)&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives the ratio directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;k = 232 / 310 = 0.7484
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Viewport was 1702 CSS px wide; the screenshots came back ~1274 px. &lt;code&gt;1274 / 1702 ≈ 0.7485&lt;/code&gt;. It matched.&lt;/p&gt;

&lt;p&gt;From then on, every click went through one conversion:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.7484&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// measure this yourself; do not copy mine&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;clickPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;K&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every interaction that had failed for the previous hour started working on the first try — dropdowns, confirmation dialogs, menu items.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson isn't the number.&lt;/strong&gt; &lt;code&gt;k&lt;/code&gt; depends on your window size, DPI, and tool. The lesson is that &lt;strong&gt;you should never trust a coordinate you didn't verify with a probe&lt;/strong&gt;, because this class of bug produces no error at any layer.&lt;/p&gt;

&lt;p&gt;Run the probe once at the start of a session. It costs three seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 2: the menu that was below the fold
&lt;/h2&gt;

&lt;p&gt;With clicks fixed, one menu item still did nothing. Same probe logic applied — &lt;code&gt;elementFromPoint&lt;/code&gt; returned &lt;code&gt;null&lt;/code&gt; for it.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                  &lt;span class="c1"&gt;// =&amp;gt; 981&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// =&amp;gt; 876&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dropdown extended past the bottom of the viewport. &lt;code&gt;getBoundingClientRect()&lt;/code&gt; happily returns coordinates for content that isn't on screen, and a click at y=981 in a 876px viewport goes nowhere.&lt;/p&gt;

&lt;p&gt;Scroll first, then re-measure. Never cache coordinates across a scroll:&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="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&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;r2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// re-read, always&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Failure mode 3: file inputs that don't exist yet
&lt;/h2&gt;

&lt;p&gt;One site had an upload drop zone with &lt;strong&gt;no file input in the DOM&lt;/strong&gt;:&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input[type=file]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// =&amp;gt; 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The input is created on demand when you click the zone. So: click the zone, &lt;em&gt;then&lt;/em&gt; look. To catch the element before the app calls &lt;code&gt;.click()&lt;/code&gt; on it (which opens a native dialog you can't drive), patch &lt;code&gt;createElement&lt;/code&gt; first:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rest&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;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;orig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&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;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;0&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;el&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now click the zone and the input is waiting for you in &lt;code&gt;window.__created&lt;/code&gt;. Populate it with a &lt;code&gt;DataTransfer&lt;/code&gt; and dispatch &lt;code&gt;change&lt;/code&gt;:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataTransfer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bubbles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This worked on two of the sites I tried. On the third it didn't — see below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 4: synthetic drops that React ignores
&lt;/h2&gt;

&lt;p&gt;Some drop zones aren't inputs at all. The obvious move is to synthesize the drop:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataTransfer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&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;type&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dragenter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dragover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;drop&lt;/span&gt;&lt;span class="dl"&gt;'&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;ev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DragEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bubbles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cancelable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defineProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dataTransfer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tried this against every plausible target — the drop zone, its ancestors, &lt;code&gt;document&lt;/code&gt;, &lt;code&gt;window&lt;/code&gt;. Nothing. The editor never registered a file.&lt;/p&gt;

&lt;p&gt;To be sure the &lt;em&gt;file&lt;/em&gt; wasn't the problem, I generated one in-page so there was no transfer step at all:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fillRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;800&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;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.8&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;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test.jpg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same result. That's a real boundary: for that editor, drag-and-drop needs genuine OS-level input, and no amount of event synthesis substitutes for it. I stopped and did those uploads by hand.&lt;/p&gt;

&lt;p&gt;Knowing &lt;em&gt;where the wall is&lt;/em&gt; is worth more than another hour of clever attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 5: localhost is still mixed content
&lt;/h2&gt;

&lt;p&gt;To avoid shipping image bytes through a JS payload, I served them locally with CORS enabled and fetched them from the page:&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;H&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SimpleHTTPRequestHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;end_headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Origin&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;*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;end_headers&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;curl&lt;/code&gt; confirmed the header. From an &lt;code&gt;https://&lt;/code&gt; page, the fetch hung until it timed out:&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://127.0.0.1:8941/img1.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// never resolves&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;code&gt;https&lt;/code&gt; page fetching &lt;code&gt;http://127.0.0.1&lt;/code&gt; is mixed content. The CORS header is irrelevant — the request never gets far enough to matter. Terminate that path early instead of debugging your server.&lt;/p&gt;

&lt;h2&gt;
  
  
  The preflight I now run first
&lt;/h2&gt;

&lt;p&gt;Five minutes of setup that would have saved me most of a day:&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;// 1. Coordinate probe — is my click space the page's space?&lt;/span&gt;
&lt;span class="c1"&gt;//    (inject the test button above, click its center, read window.__clickOK)&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Is the target actually on screen?&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onScreen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;&amp;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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHeight&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Does this page even have the input I assume it has?&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input[type=file]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 4. Verify before every destructive click.&lt;/span&gt;
&lt;span class="c1"&gt;//    Hover first, then confirm what is under the cursor:&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elementFromPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 4 matters more than it looks. In one menu, &lt;strong&gt;"Delete" sat directly below "Unpublish"&lt;/strong&gt; — 24 pixels apart. With a 25% coordinate error, "unpublish" lands on "delete". Hovering and reading back the element text before committing turns a destructive misfire into a no-op.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell my past self
&lt;/h2&gt;

&lt;p&gt;The productive shift wasn't a better selector strategy. It was refusing to reason about why a click "didn't work" and instead &lt;strong&gt;making the page report what it actually received&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every one of these five failures is invisible from the outside: success-shaped tool output, no exception, no console error. The only reliable signal came from instrumenting the page and reading a counter.&lt;/p&gt;

&lt;p&gt;If your automation is mysteriously doing nothing, don't start with the selector. Start with the probe.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All measurements here come from a single session driving Chrome against live marketplace sites. The scaling factor &lt;code&gt;k&lt;/code&gt; is specific to that window and tool — measure your own.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Building an Incremental Financial Data Pipeline on a Free Tier (¥0 Infrastructure)</title>
      <dc:creator>Hidenari Fujiwara</dc:creator>
      <pubDate>Fri, 14 Aug 2026 12:59:58 +0000</pubDate>
      <link>https://dev.to/hidenari/building-an-incremental-financial-data-pipeline-on-a-free-tier-y0-infrastructure-26fg</link>
      <guid>https://dev.to/hidenari/building-an-incremental-financial-data-pipeline-on-a-free-tier-y0-infrastructure-26fg</guid>
      <description>&lt;p&gt;&lt;em&gt;My stock research pipeline ingests prices and financials for every listed company in Japan, runs event-study backtests, and tracks a live paper portfolio — on delayed free-tier data, a laptop, and an infrastructure bill of exactly zero yen. This is the architecture, and the design decisions that made "free" actually work.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;When people hear "financial data pipeline," they picture streaming infrastructure and four-figure data subscriptions. My entire setup is: one official free API, Python, parquet files, and a macOS scheduler. It has processed &lt;strong&gt;276 small-cap stocks and 1,385 earnings events&lt;/strong&gt; through backtests, and it runs unattended every trading day.&lt;/p&gt;

&lt;p&gt;The unlock wasn't a clever tool. It was accepting one constraint and designing around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Why delayed data + a free tier is actually practical
&lt;/h2&gt;

&lt;p&gt;My data source is &lt;a href="https://dev.to/hidenari/japans-stock-exchange-has-an-official-api-for-individuals-almost-nobody-outside-japan-knows-about-4p08"&gt;J-Quants, the Japan Exchange Group's official API&lt;/a&gt;, whose free tier delays data by 12 weeks. That sounds disqualifying — until you separate what a research pipeline actually does into two jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Research&lt;/strong&gt; (backtesting, screening, factor analysis): operates on &lt;em&gt;history&lt;/em&gt;. A 12-week delay is irrelevant when you're studying six years of earnings events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution&lt;/strong&gt; (what's the price right now?): needs &lt;em&gt;today&lt;/em&gt;. But it needs today for only ~20 tickers in my portfolio, not the whole market — and a free quotes library (&lt;code&gt;yfinance&lt;/code&gt;) covers that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping these two concerns in separate modules means the expensive requirement (fresh data) shrinks to a tiny surface area, and the bulky requirement (deep history, full market) lands on the free tier. Most hobby-quant projects die by paying real-time prices for a research problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Ingestion: assume the API hates you
&lt;/h2&gt;

&lt;p&gt;The retrieval layer makes three assumptions that turned out to be load-bearing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Responses are paginated&lt;/strong&gt; — follow the &lt;code&gt;pagination_key&lt;/code&gt; until it's gone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You will get rate-limited&lt;/strong&gt; — exponential backoff on HTTP 429.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Even polite clients should throttle&lt;/strong&gt; — a minimum 250ms between requests, &lt;em&gt;on top of&lt;/em&gt; backoff, because a full-universe pull is thousands of calls and you want to be a good citizen of an API you depend on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I walked through the client code in &lt;a href="https://dev.to/hidenari/japans-stock-exchange-has-an-official-api-for-individuals-almost-nobody-outside-japan-knows-about-4p08"&gt;the J-Quants guide&lt;/a&gt;, so here I'll just note the architectural point: &lt;strong&gt;ingestion is its own layer with no business logic in it.&lt;/strong&gt; It takes an endpoint and parameters, returns rows, and knows nothing about stocks. Every time I've blurred that boundary in past projects, changing a research question forced me to touch (and break) download code.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The cache layer: misses are data too
&lt;/h2&gt;

&lt;p&gt;Everything the API returns is written to parquet, keyed by request. The interesting decision — the one that halved my backtest runtime — was what to do when the API returns &lt;em&gt;nothing&lt;/em&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_cached&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&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="n"&gt;fetch_fn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CACHE_DIR&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.parquet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&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;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;df&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="nf"&gt;read_parquet&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="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;    &lt;span class="c1"&gt;# a cached "nothing" is an answer
&lt;/span&gt;    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_parquet&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="c1"&gt;# persist even when empty
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Financial data is &lt;em&gt;full&lt;/em&gt; of legitimate emptiness: delisted tickers, market holidays, companies that didn't exist yet, quarters with no disclosure. My first backtest run spent roughly half its wall-clock time re-asking the API questions whose true answer was "nothing," every single run. Caching the misses — persisting empty frames instead of only successful payloads — cut runtime in half, with zero change to results.&lt;/p&gt;

&lt;p&gt;The general lesson: &lt;strong&gt;"no data" is a fact about the world, and facts are cacheable.&lt;/strong&gt; Most caching tutorials only cache hits.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Incremental updates — and what tools like Airbyte formalize
&lt;/h2&gt;

&lt;p&gt;With a cold cache, a full-universe pull takes a long evening. With a warm one, the daily update is minutes, because the pipeline only fetches what it doesn't already have: the cache key encodes &lt;em&gt;(endpoint, ticker, period)&lt;/em&gt;, so yesterday's history is never re-downloaded, and each day appends only the new slice.&lt;/p&gt;

&lt;p&gt;That's an &lt;strong&gt;incremental sync&lt;/strong&gt; — hand-rolled. If you've used a data-integration platform like Airbyte, you'll recognize everything above as things it formalizes: sources with pagination and rate-limit handling, destinations, cursor fields ("what did I fetch last?"), incremental vs. full-refresh modes. Building it by hand taught me exactly what those abstractions are for — and where my version is weaker (my "cursor" is a filename convention; state lives in a directory listing; there's no schema migration story). If this pipeline ever outgrows one laptop, the migration path is clear: my ingest layer becomes a declarative source config, parquet becomes DuckDB or Postgres, and the cache logic I hand-wrote disappears into the platform's sync engine. The architecture translates one-to-one — because it's the same problem.&lt;/p&gt;

&lt;p&gt;For a personal-scale project, though, the file-based version has a real virtue: &lt;strong&gt;you can &lt;code&gt;ls&lt;/code&gt; your state.&lt;/strong&gt; Debugging a sync issue is &lt;code&gt;open cache/8035_daily.parquet&lt;/code&gt;, not spelunking through a job orchestrator.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Two pitfalls that produce beautiful, wrong results
&lt;/h2&gt;

&lt;p&gt;Both of these produced exciting "findings" that died under inspection:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stock splits make fundamentals look broken.&lt;/strong&gt; Splits arrive in the disclosure stream; if you join fundamentals to prices naively, a 1:3 split reads as earnings collapsing by two-thirds — or, joined the other way, as a stock suddenly trading at a third of its "fair value." My event studies had to adjust for split factors &lt;em&gt;before&lt;/em&gt; computing any surprise or valuation metric. If your backtest finds that stocks crater on earnings and then recover perfectly, check your split handling before you check your genius.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One-off gains make garbage look cheap.&lt;/strong&gt; Company-issued forecasts make forward-valuation screens easy — and easily fooled. A company that sold its headquarters building shows a spectacular earnings yield for exactly one year. My screen excludes implausible earnings yields on the theory that if a small-cap looks &lt;em&gt;too&lt;/em&gt; cheap, it's usually an asset sale, not a misunderstood business. That single guard removed most of the false positives from the cheapest quintile.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it all costs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;J-Quants free tier: ¥0&lt;/li&gt;
&lt;li&gt;Quotes for ~20 live tickers: ¥0&lt;/li&gt;
&lt;li&gt;Storage: a few GB of parquet on my laptop: ¥0&lt;/li&gt;
&lt;li&gt;Scheduling: &lt;a href="https://dev.to/hidenari/launchd-the-macos-scheduler-you-already-have-a-practical-tutorial-553l"&gt;launchd, which macOS already runs&lt;/a&gt;: ¥0&lt;/li&gt;
&lt;li&gt;Compute: the laptop I already own&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The constraint that made this possible wasn't budget discipline. It was the architectural split in section 1 — once research and execution stopped sharing a data requirement, every remaining piece had a free-tier-shaped hole it fit into.&lt;/p&gt;

&lt;p&gt;Whether the &lt;em&gt;strategies&lt;/em&gt; this pipeline tests actually make money is a different story — &lt;a href="https://dev.to/hidenari/i-asked-claude-code-to-find-me-a-trading-edge-it-killed-three-strategies-first-22ai"&gt;most of mine didn't survive their own backtests&lt;/a&gt; — but that's the point of building the pipeline first: cheap, fast falsification beats expensive conviction.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Data is used under J-Quants' personal-use license. Nothing here is investment advice — the pipeline exists precisely because I don't trust my own hunches.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>datascience</category>
      <category>architecture</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>launchd: The macOS Scheduler You Already Have (A Practical Tutorial)</title>
      <dc:creator>Hidenari Fujiwara</dc:creator>
      <pubDate>Fri, 14 Aug 2026 12:32:46 +0000</pubDate>
      <link>https://dev.to/hidenari/launchd-the-macos-scheduler-you-already-have-a-practical-tutorial-553l</link>
      <guid>https://dev.to/hidenari/launchd-the-macos-scheduler-you-already-have-a-practical-tutorial-553l</guid>
      <description>&lt;p&gt;&lt;em&gt;Cron exists on macOS, but Apple has been quietly steering everyone toward launchd for twenty years. Here's a hands-on guide to running your scripts on a schedule — with two real jobs I run every week, and the mistakes I made so you don't have to.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I run two scheduled jobs on my Mac that quietly do work for me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every weekday at 15:45 (after the Tokyo market close), a job records prices for a paper-trading portfolio and sends me a macOS notification with the day's P&amp;amp;L.&lt;/li&gt;
&lt;li&gt;Every Monday at 09:00, a job pulls my dev.to article stats into a CSV so I can see trends without checking dashboards.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total infrastructure cost: ¥0. No cloud, no serverless, no Docker. Just &lt;code&gt;launchd&lt;/code&gt;, the init system that is already running on your Mac. This tutorial builds up a working job from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Understand the one file that matters
&lt;/h2&gt;

&lt;p&gt;A launchd job is defined by a single XML file (a "property list" / plist) in &lt;code&gt;~/Library/LaunchAgents/&lt;/code&gt;. The naming convention is reverse-DNS: &lt;code&gt;com.yourname.yourjob.plist&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is a minimal, real one — my weekly stats job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;plist&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Label&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;com.example.devto-stats&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;ProgramArguments&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/usr/bin/python3&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/Users/you/projects/writing/scripts/devto_stats.py&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StartCalendarInterval&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Weekday&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;1&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;  &lt;span class="c"&gt;&amp;lt;!-- Monday --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Hour&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;9&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Minute&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;0&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StandardOutPath&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/Users/you/projects/writing/data/launchd.log&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StandardErrorPath&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/Users/you/projects/writing/data/launchd.log&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plist&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four things to notice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Label&lt;/code&gt;&lt;/strong&gt; must be unique on your machine and should match the filename.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ProgramArguments&lt;/code&gt;&lt;/strong&gt; is an array: binary first, then each argument as its own &lt;code&gt;&amp;lt;string&amp;gt;&lt;/code&gt;. This is the #1 source of silent failures — &lt;code&gt;python3 script.py&lt;/code&gt; as a single string will not work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;StartCalendarInterval&lt;/code&gt;&lt;/strong&gt; is cron's schedule, but readable. Omit &lt;code&gt;Weekday&lt;/code&gt; to run daily; use an array of dicts for multiple times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always set the log paths.&lt;/strong&gt; launchd failures are invisible without them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: Load it, run it, prove it works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Load the job (modern syntax; "load" is the deprecated spelling)&lt;/span&gt;
launchctl bootstrap gui/&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; ~/Library/LaunchAgents/com.example.devto-stats.plist

&lt;span class="c"&gt;# Is it registered?&lt;/span&gt;
launchctl list | &lt;span class="nb"&gt;grep &lt;/span&gt;devto-stats

&lt;span class="c"&gt;# Don't wait for Monday — force a run right now:&lt;/span&gt;
launchctl kickstart gui/&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/com.example.devto-stats

&lt;span class="c"&gt;# Watch what happened:&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/projects/writing/data/launchd.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To unload: &lt;code&gt;launchctl bootout gui/$(id -u)/com.example.devto-stats&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kickstart&lt;/code&gt; is the debugging superpower. Edit script → kickstart → read log, in a tight loop, instead of waiting for the schedule to come around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Write scripts that survive being run by a robot
&lt;/h2&gt;

&lt;p&gt;A script that works in your terminal will betray you under launchd, because launchd gives it a minimal environment: no shell profile, no &lt;code&gt;PATH&lt;/code&gt; additions, no working directory you expect, no API keys you exported once in &lt;code&gt;.zshrc&lt;/code&gt;. Rules I follow after learning each one the hard way:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Absolute paths for everything.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/zsh&lt;/span&gt;
&lt;span class="nv"&gt;REPO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/Users/you/projects/trading"&lt;/span&gt;     &lt;span class="c"&gt;# not ~/projects, not $PWD&lt;/span&gt;
&lt;span class="nv"&gt;PY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;/.venv/bin/python"&lt;/span&gt;            &lt;span class="c"&gt;# the venv's python, not "python3"&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Read secrets from files, not from the environment.&lt;/strong&gt; My stats script reads its API key from a git-ignored file and exits politely when it's missing, so the job is safe to install before it's configured:&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="n"&gt;KEY_FILE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ROOT&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.devto_api_key&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;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;KEY_FILE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API key not set. Skipping.&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="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KEY_FILE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Make re-runs harmless (idempotency).&lt;/strong&gt; Scheduled jobs get run twice — by you while debugging, by launchd after a missed schedule. My market-tracking job records positions only if they haven't been recorded already:&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="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ticker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;journal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open_positions&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
&lt;span class="n"&gt;new_rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ticker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;   &lt;span class="c1"&gt;# re-running adds nothing
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Append to logs with a timestamp header&lt;/strong&gt;, so the log file reads as a history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"===== &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%H:%M&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; ====="&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; tracker.py report
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Close the loop with notifications
&lt;/h2&gt;

&lt;p&gt;A scheduled job you never hear from is a job you'll forget exists — until it's been broken for a month. The cheapest feedback channel on macOS is a notification, one line of AppleScript away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;summary&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Portfolio"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
osascript &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"display notification &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;summary&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;Job&lt;/span&gt;&lt;span class="p"&gt; failed, check log&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  with title &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Daily tracker&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the fallback text: if the report failed and &lt;code&gt;$summary&lt;/code&gt; is empty, the notification says so instead of silently not appearing. The &lt;code&gt;|| true&lt;/code&gt; keeps a notification failure from failing the whole job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Know when &lt;em&gt;not&lt;/em&gt; to use launchd
&lt;/h2&gt;

&lt;p&gt;Honest limitations, from running these jobs for a while:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your Mac must be awake.&lt;/strong&gt; launchd runs missed &lt;code&gt;StartCalendarInterval&lt;/code&gt; jobs when the machine wakes — but only the most recent miss, and "sometime after wake" may be too late for time-sensitive work. My 15:45 market job assumes the laptop is open in the afternoon; that assumption holds for me, but a server it is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No retries, no alerting, no history UI.&lt;/strong&gt; You are the monitoring system (hence Step 4).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the job must run at an exact minute regardless of your laptop's mood&lt;/strong&gt;, use a cloud scheduler. The point of launchd is the enormous class of personal automation where "today, roughly on time, for free, with my local files and credentials" is exactly right.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part where this compounds
&lt;/h2&gt;

&lt;p&gt;Each job is small. The pattern is what compounds: every repetitive check in your life — a dashboard you open, a number you copy into a spreadsheet, a daily record you keep meaning to keep — is a candidate for a 30-line script and a plist. I now write the script, wire the plist, watch it via notifications for a week, and forget about it.&lt;/p&gt;

&lt;p&gt;My laptop does the remembering. That's the whole trick.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Both jobs in this article are simplified from real ones I run: a paper-trading tracker for a rule-based stock research project (&lt;a href="https://dev.to/hidenari/i-asked-claude-code-to-find-me-a-trading-edge-it-killed-three-strategies-first-22ai"&gt;build log&lt;/a&gt;) and a writing-stats collector. Simplifications don't change behavior.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>macos</category>
      <category>automation</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Japan's Stock Exchange Has an Official API for Individuals. Almost Nobody Outside Japan Knows About It.</title>
      <dc:creator>Hidenari Fujiwara</dc:creator>
      <pubDate>Fri, 14 Aug 2026 12:05:31 +0000</pubDate>
      <link>https://dev.to/hidenari/japans-stock-exchange-has-an-official-api-for-individuals-almost-nobody-outside-japan-knows-about-4p08</link>
      <guid>https://dev.to/hidenari/japans-stock-exchange-has-an-official-api-for-individuals-almost-nobody-outside-japan-knows-about-4p08</guid>
      <description>&lt;p&gt;&lt;em&gt;A practical developer's guide to J-Quants — the Japan Exchange Group's official market data API — from someone who has been running backtests and a daily pipeline on it for months.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;If you've ever tried to analyze the Japanese stock market programmatically from outside Japan, you know the data situation looks hostile. Bloomberg is enterprise money. &lt;code&gt;yfinance&lt;/code&gt; gives you prices but its Japanese fundamentals are patchy. Most tutorials just tell you to trade the US market instead.&lt;/p&gt;

&lt;p&gt;Here's what almost nobody writes about in English: &lt;strong&gt;the Tokyo Stock Exchange's parent company (JPX) runs an official data API for individual investors, and the free tier is genuinely useful.&lt;/strong&gt; It's called &lt;a href="https://jpx-jquants.com/en" rel="noopener noreferrer"&gt;J-Quants&lt;/a&gt;, it has an English version of the site, and I've been using it for months as the backbone of a rule-based research pipeline (&lt;a href="https://dev.to/hidenari/i-asked-claude-code-to-find-me-a-trading-edge-it-killed-three-strategies-first-22ai"&gt;the build log is here&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;This is the guide I wish had existed when I started.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get, and what it costs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price/month&lt;/th&gt;
&lt;th&gt;History&lt;/th&gt;
&lt;th&gt;Catch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Free&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;¥0&lt;/td&gt;
&lt;td&gt;2 years&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;12-week delay&lt;/strong&gt; on data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Light&lt;/td&gt;
&lt;td&gt;¥1,650 (~$11)&lt;/td&gt;
&lt;td&gt;5 years&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;¥3,300 (~$22)&lt;/td&gt;
&lt;td&gt;10 years&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium&lt;/td&gt;
&lt;td&gt;¥16,500 (~$110)&lt;/td&gt;
&lt;td&gt;20 years&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 12-week delay on the free tier sounds disqualifying until you realize what it's actually for: &lt;strong&gt;backtesting&lt;/strong&gt;. Historical research doesn't care about the delay. For live tracking I fill the gap with &lt;code&gt;yfinance&lt;/code&gt; — delayed official data for the heavy lifting, free real-ish-time quotes for marking to market. That combination costs ¥0.&lt;/p&gt;

&lt;p&gt;One thing to check before you build anything: the license is &lt;strong&gt;personal use&lt;/strong&gt;. No redistribution, no commercial resale of the data. Build your own research tools, not a public data product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API in three endpoints
&lt;/h2&gt;

&lt;p&gt;The current version (V2) uses plain API-key auth — a &lt;code&gt;x-api-key&lt;/code&gt; header, no token-refresh dance. The three endpoints I use daily:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/v2/equities/master&lt;/code&gt; — all listed companies: code, name, market segment, sector, size classification&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/v2/equities/bars/daily&lt;/code&gt; — daily OHLCV&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/v2/fins/summary&lt;/code&gt; — financial statements, &lt;strong&gt;including company-issued forecasts&lt;/strong&gt; (more on why that's special below)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal client that handles the two things the docs won't hold your hand through — pagination and rate limits:&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;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.jquants.com/v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;HEADERS&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;x-api-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;YOUR_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_all&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&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;list&lt;/span&gt;&lt;span class="p"&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;Follow pagination_key until exhausted; back off on 429s.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;                      &lt;span class="c1"&gt;# 429 -&amp;gt; exponential backoff
&lt;/span&gt;            &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;path&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="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                             &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;data_key&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;# payload key varies by endpoint
&lt;/span&gt;            &lt;span class="n"&gt;data_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pagination_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rows&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;j&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;data_key&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;j&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;pagination_key&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="n"&gt;rows&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pagination_key&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;j&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pagination_key&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;Two production notes baked into that snippet:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The payload key changes per endpoint&lt;/strong&gt; (&lt;code&gt;bars&lt;/code&gt;, &lt;code&gt;summary&lt;/code&gt;, …), so detect it instead of hardcoding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throttle yourself.&lt;/strong&gt; I keep a minimum 250ms between requests on top of the backoff; full-universe pulls run for a while and the API will 429 you without it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The feature US-market people don't believe: company-issued guidance
&lt;/h2&gt;

&lt;p&gt;Here's the structurally interesting part. Japanese listed companies publish &lt;strong&gt;their own earnings forecasts&lt;/strong&gt; as part of standard disclosure — next fiscal year's revenue, operating profit, EPS, straight from management, machine-readable in &lt;code&gt;/v2/fins/summary&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you've only worked with US data, let that sink in: no scraping analyst consensus, no paying a vendor for estimates. The company itself tells you what it expects, updates it when things change, and the exchange serves it to you in JSON.&lt;/p&gt;

&lt;p&gt;This enables research that's genuinely hard to do cheaply elsewhere. My own use: post-earnings drift studies keyed on &lt;em&gt;guidance revisions&lt;/em&gt; (did management raise the forecast?) and value screens using &lt;em&gt;forward&lt;/em&gt; EPS from the source rather than trailing numbers. Whether those strategies survive scrutiny is a separate question — &lt;a href="https://dev.to/hidenari/i-asked-claude-code-to-find-me-a-trading-edge-it-killed-three-strategies-first-22ai"&gt;mine mostly didn't&lt;/a&gt; — but the &lt;em&gt;data access&lt;/em&gt; is remarkable for ¥0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas from months of production use
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cache aggressively, including empty responses.&lt;/strong&gt; My first full-universe backtest spent half its runtime re-requesting data that legitimately didn't exist (delisted names, holidays, pre-IPO dates). The fix that mattered wasn't caching hits — it was caching &lt;em&gt;misses&lt;/em&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_cached&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&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="n"&gt;fetch_fn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CACHE_DIR&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.parquet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&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;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;df&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="nf"&gt;read_parquet&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="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;       &lt;span class="c1"&gt;# cached "nothing" is an answer too
&lt;/span&gt;    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_parquet&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="c1"&gt;# persist even when empty
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Corporate actions will bite your event studies.&lt;/strong&gt; Stock splits show up in disclosures; if you join fundamentals to prices naively, a 1:3 split looks like earnings collapsing by two-thirds. Adjust before you celebrate a "signal."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guard against one-off earnings spikes.&lt;/strong&gt; Company forecasts make forward-valuation screens easy, but a company selling its headquarters building looks spectacularly "cheap" for exactly one year. My screens exclude implausible earnings yields — if it looks too good, it's usually an asset sale, not a business.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free-tier delay shapes your architecture.&lt;/strong&gt; Twelve weeks of delay means the free tier can't tell you about &lt;em&gt;this&lt;/em&gt; quarter. Design around it: J-Quants for research and screening, a real-time source for execution-time prices. Keeping those two concerns in separate modules made this painless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it worth it?
&lt;/h2&gt;

&lt;p&gt;For anyone who wants to do serious, reproducible research on a major market without a data budget: yes, emphatically. An official exchange API with fundamentals &lt;em&gt;and&lt;/em&gt; management guidance, a usable free tier, pandas-friendly JSON, and an English site — I don't know of an equivalent offer from any other major exchange.&lt;/p&gt;

&lt;p&gt;The Japanese market itself is having a moment with international investors. The tooling to study it properly is sitting right there, mostly undocumented in English. Now you know.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I use J-Quants under its personal-use license for my own research. Nothing here is investment advice. If you build something with it, I'd genuinely like to hear about it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>datascience</category>
      <category>finance</category>
    </item>
    <item>
      <title>I Asked Claude Code to Find Me a Trading Edge. It Killed Three Strategies First.</title>
      <dc:creator>Hidenari Fujiwara</dc:creator>
      <pubDate>Fri, 14 Aug 2026 07:26:03 +0000</pubDate>
      <link>https://dev.to/hidenari/i-asked-claude-code-to-find-me-a-trading-edge-it-killed-three-strategies-first-22ai</link>
      <guid>https://dev.to/hidenari/i-asked-claude-code-to-find-me-a-trading-edge-it-killed-three-strategies-first-22ai</guid>
      <description>&lt;p&gt;&lt;em&gt;How I built a fully automated, rule-based Japanese stock research pipeline with Claude Code, J-Quants, and a macOS cron job — and why "the AI is not allowed to predict anything" turned out to be the most useful constraint in the whole project.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Everyone's first idea for "AI + stocks" is the same: ask the model whether a stock will go up. That idea has been tested, and it fails — LLMs guessing post-event price direction land around coin-flip accuracy. I'm a freelance web developer in Japan, not a quant, and I knew that if I let an LLM "predict" anything I would just be laundering my own wishful thinking through a chatbot.&lt;/p&gt;

&lt;p&gt;So I gave Claude Code a different job description. In my project's &lt;code&gt;CLAUDE.md&lt;/code&gt; — the standing instructions file the agent reads every session — there's a hard rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;No predictions. Data only.&lt;/strong&gt; The AI's role is limited to four things: (1) structuring data, (2) computing factual metrics, (3) checking facts against pre-defined rules, (4) explaining results with sources. Trading decisions are rule-based. Overfitting to past data is a hidden prediction — prefer economically sensible rules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude Code's job was to be the engineer: build the data pipeline, implement backtests I specified, and then — this is the important part — &lt;strong&gt;kill my ideas with evidence&lt;/strong&gt;. Over a few weeks it killed three of them. Here's the honest record.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;Nothing exotic. The point was cheap and reproducible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;J-Quants API&lt;/strong&gt; — the official Japan Exchange data service. The free personal tier has a 12-week delay on prices, which is fine for backtesting; I fill in current prices with &lt;code&gt;yfinance&lt;/code&gt; for live tracking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python + pandas&lt;/strong&gt; for backtests, with a local file cache so repeated runs don't hammer the API. (One lesson learned: cache &lt;em&gt;empty&lt;/em&gt; responses too. Half my early runtime was re-requesting data that legitimately didn't exist.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code&lt;/strong&gt; as the pair engineer. I describe the rule, it writes the backtest, I interrogate the output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;launchd&lt;/strong&gt; (macOS's cron) for the daily tracking job. No cloud, no server bill.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Round 1: technical strategies — dead
&lt;/h2&gt;

&lt;p&gt;First I had Claude Code implement the classics on Nikkei-universe data: breakout entries, RSI mean-reversion, with realistic costs and taxes included.&lt;/p&gt;

&lt;p&gt;Ten-year portfolio result: &lt;strong&gt;+15.4%&lt;/strong&gt; total. Sounds okay until you put it next to the benchmark: buy-and-holding the Nikkei over the same period returned &lt;strong&gt;+138.8%&lt;/strong&gt; (with a −26% max drawdown). My "strategy" wasn't a strategy; it was an expensive way to sit out a bull market.&lt;/p&gt;

&lt;p&gt;Verdict: dead. Next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round 2: earnings drift (PEAD) — killed by a single filter
&lt;/h2&gt;

&lt;p&gt;Post-earnings announcement drift is one of the best-documented anomalies: stocks that beat guidance keep drifting up for weeks. On large caps, my backtest found nothing — no monotonicity across surprise sizes, slightly negative drift everywhere. Institutional money has eaten that edge.&lt;/p&gt;

&lt;p&gt;Small caps looked genuinely exciting: 276 stocks, 1,385 earnings events, 423 trades. The threshold sweep was beautifully monotonic — bigger guidance beats, bigger drift: +1.6% → +2.1% → +2.7% → +3.4% per trade as the threshold rose.&lt;/p&gt;

&lt;p&gt;Then I asked Claude Code to add one boring, adult filter: &lt;strong&gt;only keep stocks with at least ¥100M in daily trading value&lt;/strong&gt; — i.e., stocks I could actually buy without moving the price.&lt;/p&gt;

&lt;p&gt;The edge evaporated: &lt;strong&gt;−0.34%&lt;/strong&gt; per trade on the 33 surviving trades. At ¥300M/day it got worse (−2.48%).&lt;/p&gt;

&lt;p&gt;The anomaly was real, but it lives exclusively in stocks too illiquid to trade at size. This was the single most valuable chart the pipeline ever produced, and it's a &lt;em&gt;negative&lt;/em&gt; result. If your backtest doesn't include an executability filter, it isn't a backtest — it's fan fiction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round 3: small-cap value × quality — the survivor
&lt;/h2&gt;

&lt;p&gt;Third idea: a quarterly-rebalanced screen. Rank small caps by valuation (with quality guards: equity ratio ≥ 25%, positive forecast EPS, and a filter against one-off earnings spikes), split into quintiles, hold the cheapest.&lt;/p&gt;

&lt;p&gt;This one behaved differently. The cheapest quintile (Q1) returned &lt;strong&gt;+8.89% per quarter&lt;/strong&gt;, returns were fully monotonic down the ranks, the cheap-vs-expensive spread was +8.96% per quarter, and Q1 beat the universe average in &lt;strong&gt;all six&lt;/strong&gt; backtest quarters.&lt;/p&gt;

&lt;p&gt;And the crucial difference from PEAD: &lt;strong&gt;the edge survived the liquidity filter.&lt;/strong&gt; Restricted to ≥¥100M/day stocks, Q1 still returned +10.06% per quarter (+78.7% cumulative vs +42.2% for the universe). The sign didn't flip.&lt;/p&gt;

&lt;p&gt;One strategy out of three survived its own audit. That's the pipeline working as intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating the paper trade
&lt;/h2&gt;

&lt;p&gt;A backtest that survives in-sample is still just a hypothesis, so the surviving strategy went into "Phase 0": a paper portfolio tracked automatically every trading day, with a pre-committed bar to clear — beat the benchmark over 2–3 months &lt;em&gt;without touching the parameters&lt;/em&gt; — before real money scales beyond pocket change.&lt;/p&gt;

&lt;p&gt;The automation is deliberately low-tech. A launchd job fires at 15:45 JST on weekdays (after the Tokyo close) and runs a shell script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/zsh&lt;/span&gt;
&lt;span class="c"&gt;# Runs from launchd on weekdays at 15:45 JST.&lt;/span&gt;
&lt;span class="c"&gt;# 1) Record any not-yet-entered positions at today's opening price&lt;/span&gt;
&lt;span class="c"&gt;# 2) Append the daily report to the log, push a summary to macOS notifications&lt;/span&gt;
&lt;span class="nv"&gt;REPO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/work/makemoney"&lt;/span&gt;
&lt;span class="nv"&gt;PY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;/.venv/bin/python"&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"===== &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; ====="&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; phase0_track.py open &lt;span class="nt"&gt;--date&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;report&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; phase0_track.py report&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$report&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;/data/phase0_daily.log"&lt;/span&gt;

&lt;span class="nv"&gt;summary&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$report&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Portfolio"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
osascript &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"display notification &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$summary&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; with title &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Phase 0&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Python side is a small CLI with two subcommands. &lt;code&gt;open&lt;/code&gt; records entries at the day's opening price (same conditions as the fractional-share "opening auction" orders I'd use with real money) with an idempotency guard, so re-running never double-records:&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;# double-entry guard
&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ticker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;journal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open_positions&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;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strategy&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;STRATEGY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;lst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lst&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;lst&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ticker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&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;report&lt;/code&gt; marks the portfolio to market and compares it against two benchmarks (Nikkei 225 and a TOPIX ETF) &lt;em&gt;measured from the same entry date&lt;/em&gt; — the comparison that Round 1 taught me never to skip. The result lands in a log file and a macOS notification. Total infrastructure cost: ¥0.&lt;/p&gt;

&lt;h2&gt;
  
  
  One month in: the honest numbers
&lt;/h2&gt;

&lt;p&gt;As of 2026-08-14, one month after entry (18 positions, a small paper portfolio):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Portfolio: +4.32%&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Nikkei 225: +2.55%&lt;/li&gt;
&lt;li&gt;TOPIX ETF: &lt;strong&gt;+4.66%&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Beating the Nikkei, narrowly losing to TOPIX. Inside the portfolio the dispersion is exactly what a small-cap quintile bet looks like: the best position is +46%, the worst is −33%. The daily report ends with a line I wrote for my own discipline: &lt;em&gt;"The pass bar is 2–3 months against the benchmark. Do not react to daily noise."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Maybe it clears the bar in October. Maybe it doesn't and the strategy joins the graveyard with the other three. Either outcome is the system working.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Claude Code was actually good at
&lt;/h2&gt;

&lt;p&gt;A few workflow notes for anyone trying something similar:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Put your constraints in &lt;code&gt;CLAUDE.md&lt;/code&gt;, not in your willpower.&lt;/strong&gt; "No predictions", "never commit to main", "every research claim needs a source URL" — encoded as standing project rules, the agent enforces them even when I'm tempted not to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the agent a strategy killer, not a strategy generator.&lt;/strong&gt; LLMs are dangerously good at producing plausible-sounding trading ideas. They are &lt;em&gt;actually&lt;/em&gt; useful at implementing the boring falsification machinery — cost models, liquidity filters, benchmark alignment — fast enough that you'll really run it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit each negative result.&lt;/strong&gt; My git log reads like a lab notebook: "large-cap PEAD: no edge", "PEAD edge concentrates in illiquid names", "value×quality survives liquidity filter". Claude Code picks up this context every session and stops me from re-testing yesterday's dead idea with today's optimism.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent writes the pipeline; the human owns the rules.&lt;/strong&gt; Every threshold in the system (quality guards, liquidity floor, rebalance cadence) was decided by me and frozen &lt;em&gt;before&lt;/em&gt; the evaluation window. The fastest way to fool yourself with an eager coding agent is to let it "just try a few more parameter values."&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Nothing here is investment advice — it's a build log of a personal research tool, running on pocket-money stakes precisely because the evidence isn't in yet. J-Quants data is used under its personal-use license.&lt;/em&gt;&lt;/p&gt;

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