<?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: Ryan Qi</title>
    <description>The latest articles on DEV Community by Ryan Qi (@lushenwar).</description>
    <link>https://dev.to/lushenwar</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3874105%2F97e51149-87f8-4261-8b91-81381ad6b57a.jpeg</url>
      <title>DEV Community: Ryan Qi</title>
      <link>https://dev.to/lushenwar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lushenwar"/>
    <language>en</language>
    <item>
      <title>Your risk model passes all its tests. It will still blow up in a crisis.</title>
      <dc:creator>Ryan Qi</dc:creator>
      <pubDate>Sat, 11 Apr 2026 21:19:14 +0000</pubDate>
      <link>https://dev.to/lushenwar/your-risk-model-passes-all-its-tests-it-will-still-blow-up-in-a-crisis-14po</link>
      <guid>https://dev.to/lushenwar/your-risk-model-passes-all-its-tests-it-will-still-blow-up-in-a-crisis-14po</guid>
      <description>&lt;p&gt;It's rough. There are probably bugs. But if I wait until it's perfect, I'll never post it.&lt;/p&gt;

&lt;p&gt;So here it is.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;"Black swans being unpredictable, we need to adjust to their existence rather than naively try to predict them."&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The thing that annoyed me
&lt;/h2&gt;

&lt;p&gt;I was writing a portfolio risk model a few months back. Linters happy, tests passing, everything looked fine. Ran it against historical data, still fine.&lt;/p&gt;

&lt;p&gt;Then I started wondering: what actually happens to this thing during a liquidity crisis? Like, what if correlations spike hard and vol goes through the roof at the same time? Does the math still hold?&lt;/p&gt;

&lt;p&gt;I couldn't find a clean way to answer that. Backtesting libraries exist, sure, but I wanted something that would look at the code itself and tell me &lt;em&gt;where&lt;/em&gt; it breaks and &lt;em&gt;why&lt;/em&gt;. Not just that it produced a wrong number. The exact line. The exact input. The chain of variables that led there.&lt;/p&gt;

&lt;p&gt;I couldn't find that tool so I started building it. That became BlackSwan.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;

&lt;p&gt;You point it at a Python function containing financial or numerical logic, pick a stress scenario, and it hammers the function with thousands of perturbed inputs drawn from realistic market conditions: liquidity crash, vol spike, correlation breakdown, rate shock, missing data.&lt;/p&gt;

&lt;p&gt;When it finds failures, it tells you the exact line, how often it happened, which input caused it, and traces the causal chain back to the root variable. It even suggests a fix.&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;$ &lt;/span&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; blackswan &lt;span class="nb"&gt;test &lt;/span&gt;models/risk.py &lt;span class="nt"&gt;--scenario&lt;/span&gt; liquidity_crash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shatter_points"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"critical"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"failure_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"non_psd_matrix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Covariance matrix loses positive semi-definiteness when pairwise correlation exceeds 0.91."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"frequency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"847 / 5000 iterations (16.9%)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"causal_chain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"variable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"correlation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"root_input"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"variable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"corr_matrix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"intermediate"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"variable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cov_matrix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"failure_site"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"fix_hint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Apply nearest-PSD correction (Higham 2002) after correlation perturbation, or clamp eigenvalues to epsilon."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Line 36. 16.9% failure rate under a liquidity crash scenario. That covariance matrix was quietly breaking almost 1 in 6 times and nothing in my normal workflow would have caught it. The model would have just... silently produced garbage.&lt;/p&gt;

&lt;p&gt;That's the gap BlackSwan is trying to fill. Standard testing tools understand code structure. They don't understand mathematical behavior under stress.&lt;/p&gt;




&lt;h2&gt;
  
  
  How far along is it
&lt;/h2&gt;

&lt;p&gt;Genuinely early. I want to be upfront about that.&lt;/p&gt;

&lt;p&gt;It's on PyPI (&lt;code&gt;pip install blackswan&lt;/code&gt;) and it works, but the scope is narrow by design. Right now it's focused on portfolio risk, covariance/correlation analysis, and VaR-style models using NumPy and Pandas. If you try to throw a random Python file at it, it'll reject it rather than silently produce garbage output. I think that's the right call for now.&lt;/p&gt;

&lt;p&gt;There's also a VS Code extension if you'd rather not live in the terminal. Click &lt;strong&gt;Run BlackSwan&lt;/strong&gt; above a function, pick a scenario from a dropdown, and failures show up as red squiggles with hover tooltips. That part was genuinely fun to build.&lt;/p&gt;

&lt;p&gt;The ambition outpaces where it is right now. But it's real and it's useful.&lt;/p&gt;




&lt;h2&gt;
  
  
  The interesting parts (if you want to go deeper)
&lt;/h2&gt;

&lt;p&gt;This section is for people who want to know how it actually works under the hood.&lt;/p&gt;

&lt;h3&gt;
  
  
  Detectors
&lt;/h3&gt;

&lt;p&gt;Every iteration runs up to 8 detectors concurrently on your function's output:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Detector&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NaNInfDetector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Any computation producing NaN or Inf&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MatrixPSDDetector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Covariance matrix losing positive semi-definiteness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ConditionNumberDetector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ill-conditioned matrices before inversion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DivisionStabilityDetector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Denominator approaching zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ExplodingGradientDetector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Output growing 100x relative to input perturbation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RegimeShiftDetector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Structural breaks in output distribution across iterations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BoundsDetector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Outputs exceeding configurable plausible bounds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LogicalInvariantDetector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;User-defined assertions (e.g. portfolio weights must sum to 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These get auto-tagged to relevant source lines via AST analysis. No instrumentation, no decorators, no changes to your code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adversarial mode
&lt;/h3&gt;

&lt;p&gt;Standard Monte Carlo samples perturbations randomly. Adversarial mode runs a genetic algorithm that &lt;em&gt;evolves&lt;/em&gt; stress parameters toward worst-case inputs instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; blackswan &lt;span class="nb"&gt;test &lt;/span&gt;models/risk.py &lt;span class="nt"&gt;--scenario&lt;/span&gt; liquidity_crash &lt;span class="nt"&gt;--adversarial&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It maintains a population of parameter sets, scores each by failure severity, and breeds the worst performers across generations. A &lt;code&gt;HardnessAdaptor&lt;/code&gt; automatically increases perturbation intensity when no failures are found, so it doesn't stall on robust code. It's slower but it finds things random sampling misses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Causal chains
&lt;/h3&gt;

&lt;p&gt;The output you saw above with &lt;code&gt;root_input&lt;/code&gt;, &lt;code&gt;intermediate&lt;/code&gt;, &lt;code&gt;failure_site&lt;/code&gt; comes from building a dependency graph of your function via AST analysis. When a failure is detected, BlackSwan walks that graph backwards from the failure site to find the root cause. This is the part I'm most proud of and also the part most likely to have bugs, so if the causal chains look wrong on your code please tell me.&lt;/p&gt;

&lt;h3&gt;
  
  
  ReproducibilityCard
&lt;/h3&gt;

&lt;p&gt;Every run emits a machine-readable provenance record with exact BlackSwan version, Python version, NumPy version, scenario hash, seed, and a ready-to-paste replay command. If you share a finding with a colleague and they can't reproduce it, the card tells you exactly why.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;blackswan
python &lt;span class="nt"&gt;-m&lt;/span&gt; blackswan &lt;span class="nb"&gt;test &lt;/span&gt;your_model.py &lt;span class="nt"&gt;--scenario&lt;/span&gt; vol_spike
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source on &lt;a href="https://github.com/Lushenwar/BlackSwan" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;If you try it and something breaks or doesn't make sense, open an issue. I'd much rather hear that it failed on your code than not hear anything at all. And if you work in this space and think I'm solving the wrong problem entirely, I'd genuinely like to know that too.&lt;/p&gt;




</description>
      <category>python</category>
      <category>beginners</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
