science:social:economics:finance:option-simulator

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
science:social:economics:finance:option-simulator [2025/10/04 07:52] falsycatscience:social:economics:finance:option-simulator [2025/10/04 15:56] (current) falsycat
Line 1: Line 1:
-====== Option Price Simulator ======+====== Option PnL Simulator ====== 
 + 
 +[[black-scholes-equation]]に基づいて、オプションの損益をシミュレーションするためのツールです。 
 +テキストエリアに建玉を入力してCalcボタンを押すと、縦軸を原資産価格、横軸を満期までの日数とした損益のヒートマップを生成することができます。 
 +建玉の入力記法については[[#how-to-use]]を参照してください。
  
 <html> <html>
 <style>#heatmap svg { height: auto; }</style> <style>#heatmap svg { height: auto; }</style>
-<div id="heatmap"></div> 
  
 +<div id="heatmap_param">
 +  <div><textarea class="script" style="height: 16rem">SC@22000#5$130
 +LC@21750#5$250x2
 +SC@21500#5$390</textarea></div>
 +  <div style="width: 40rem">
 +    <table>
 +      <tr><td>IV</td><td><input class="iv" type="number" value="0.2" /></td><td>1=100%</td></tr>
 +      <tr><td>Risk-Free rate</td><td><input class="risk-free-rate" type="number" value="0" /></td><td>1=100%</td></tr>
 +    </table>
 +    <button>Calc</button>
 +  </div>
 +</div>
 +
 +<div id="heatmap"></div>
  
 <script> <script>
 document.addEventListener("DOMContentLoaded", ()=>{ document.addEventListener("DOMContentLoaded", ()=>{
  
 +// error function
 const erf = (x)=>{ const erf = (x)=>{
   const p  =  0.3275911;   const p  =  0.3275911;
Line 24: Line 42:
 const normCDF = (x)=>(1 + erf(x / Math.sqrt(2))) / 2; const normCDF = (x)=>(1 + erf(x / Math.sqrt(2))) / 2;
  
 +// formula to calculate option price
 const blackScholes = (S, K, T, r, sigma, type = 'call')=>{ const blackScholes = (S, K, T, r, sigma, type = 'call')=>{
   // S : 現在の株価   // S : 現在の株価
Line 35: Line 54:
  
   if (type === 'call') {   if (type === 'call') {
-      return S * normCDF(d1) - K * Math.exp(-r * T) * normCDF(d2);+      return T>0? S * normCDF(d1) - K * Math.exp(-r * T) * normCDF(d2): Math.max(S-K, 0);
   } else if (type === 'put') {   } else if (type === 'put') {
-      return K * Math.exp(-r * T) * normCDF(-d2) - S * normCDF(-d1);+      return T>0? K * Math.exp(-r * T) * normCDF(-d2) - S * normCDF(-d1): Math.max(K-S, 0);
   } else {   } else {
       throw new Error("invalid type: "+type);       throw new Error("invalid type: "+type);
Line 44: Line 63:
  
 // simulation function // simulation function
-const simulate = ()=>{ +const simulate = (pos, r)=>{ 
-  const priceMin  39000+  const priceMin = Math.min(...pos.map(x => x.strike))*0.8
-  const priceMax  50000+  const priceMax = Math.max(...pos.map(x => x.strike))*1.2
-  const priceStep = 50+  const priceStep = (priceMax - priceMin)/100
-  const dayMax    1000+  let prices []
-  const K 38500+  for (let S priceMin; S <= priceMax; S+=priceStep) { 
-  const 0.0+    prices.push(S); 
-  const IV = 0.2;+  } 
 +   
 +  const dayMax Math.max(...pos.map(x => x.open_at))
 +  let days = []; 
 +  for (let T = 0; T <= dayMax; ++T) { 
 +    days.push(T); 
 +  }
  
-  let ret = []; +  let data = []; 
-  for (let p = priceMin; p <= priceMax; p+=priceStep) {+  for (const S of prices) {
     let row = [];     let row = [];
-    for (let dayMax; d > 0; --d) { +    for (const T of days) { 
-      row.push(blackScholes(p, K, d/365, r, IV));+      let pnl = 0; 
 +      for (const p of pos) { 
 +        if (p.open_at >= T) { 
 +            const open_vol = p.open_price; 
 +            const curr_vol = blackScholes(S, p.strikeT/252, r, p.iv, p.type)
 +            pnl += p.count*(p.side === "short"? 
 +              open_vol - curr_vol: curr_vol - open_vol); 
 +        } 
 +      } 
 +      row.push(pnl);
     }     }
-    ret.push(row);+    data.push(row);
   }   }
-  return ret;+  return [days, prices, data];
 }; };
  
-// 1. scriptタグを作成+// parse 
 +const parseScript = (script, iv)=>{ 
 +  const tokens = script.split(/\s+/); 
 +   
 +  let pos = []; 
 +  for (const tok of tokens) { 
 +    const es = tok.split(/(?=[@$#\^x])/); 
 +     
 +    const p = {}; 
 +    if (es[0] == "LC") { 
 +      p.side = "long"; 
 +      p.type = "call"; 
 +    } else if (es[0] == "LP") { 
 +      p.side = "long"; 
 +      p.type = "put"; 
 +    } else if (es[0] == "SC") { 
 +      p.side = "short"; 
 +      p.type = "call"; 
 +    } else if (es[0] == "SP") { 
 +      p.side = "short"; 
 +      p.type = "put"; 
 +    } else { 
 +      throw "fuck"; 
 +    } 
 +     
 +    const get_num = (x)=>+es.filter(s => s.startsWith(x)).at(-1)?.substring(1); 
 +    p.strike     = get_num("@"); 
 +    p.open_price = get_num("$"); 
 +    p.open_at    = get_num("#"); 
 +    p.iv         = get_num("^") || iv; 
 +    p.count      = get_num("x") || 1; 
 +     
 +    const valid = p.strike && p.open_price && p.open_at; 
 +    if (!valid) throw "fuck"; 
 +     
 +    pos.push(p); 
 +  } 
 +  return pos; 
 +}; 
 + 
 +// re-paint plot 
 +const update = ()=>{ 
 +  const params = document.querySelector("#heatmap_param"); 
 +  const script = params.querySelector(".script").value; 
 +  const iv = +params.querySelector(".iv").value; 
 +  const riskfree_rate = +params.querySelector(".risk-free-rate").value; 
 +   
 +  const pos = parseScript(script, iv); 
 +  const [x, y, z] = simulate(pos, riskfree_rate); 
 +   
 +  Plotly.newPlot('heatmap', [{ 
 +    x: x, 
 +    y: y, 
 +    z: z, 
 +    zmid: 0, 
 +    colorscale: [ 
 +      [0.0, 'blue'], 
 +      [0.49, 'lightblue'], 
 +      [0.5, 'black'], 
 +      [0.51, 'pink'], 
 +      [1.0, 'red'], 
 +    ], 
 +    type: 'heatmap', 
 +     
 +    hovertemplate: 'Days Left: %{x}<br>Underlying Price: %{y}<br>Option PnL: %{z}<extra></extra>', 
 +  }], { 
 +    title: {text: "Option PnL simulation"}, 
 +    xaxis: { 
 +      title: {text: "Days Left"}, 
 +      autorange: "reversed", 
 +      fixedrange: true, 
 +    }, 
 +    yaxis: { 
 +      title: {text: "Underlying Price"}, 
 +      fixedrange: true, 
 +    }, 
 +    dragmode: false, 
 +  }); 
 +}; 
 + 
 +// ---- main routine ----
 const script = document.createElement('script'); const script = document.createElement('script');
 script.src = "https://cdn.plot.ly/plotly-3.1.0.min.js"; script.src = "https://cdn.plot.ly/plotly-3.1.0.min.js";
 script.async = true; script.async = true;
- +script.onload = ()=>{ 
-// 2. 読み込み完了時の処理 +  document.querySelector("#heatmap_param button").addEventListener("click"update); 
-script.onload = () => { +  update();
-    // ここでPlotlyを使ったヒートマップ描画などが可能 +
-    Plotly.newPlot('heatmap'[{ +
-        z: simulate(), +
-        type: 'heatmap' +
-    }]);+
 }; };
- 
-// 3. bodyに追加してロード開始 
 document.body.appendChild(script); document.body.appendChild(script);
  
Line 84: Line 191:
 </script> </script>
 </html> </html>
 +
 +===== How To Use =====
 +
 +単体の建玉は以下の記法で記述できます。
 +単体の建玉記述を1行ずつ記述することで、複数の建玉を記述することができます。
 +
 +<code>
 +(S/L)(C/P)@strike#open_at$open_price
 +  S/L: Short or Long
 +  C/P: Call or Put
 +  strike    : strike price
 +  open_at   : days left at open
 +  open_price: price at open
 +</code>
 +
 +==== 例 ====
 +
 +オプション戦略については[[option-strategy]]を参照。
 +
 +=== Short Iron Butterfly ===
 +
 +<code>
 +SC@21000#30$990
 +LC@20000#30$590
 +LP@20000#30$620
 +SP@19000#30$120
 +</code>
  • Last modified: 17 hours ago
  • by falsycat