DEV Community

Vera Yang for BeGoodTool.com

Posted on

I thought porting my margin-call calculator to short selling was a one-line sign flip. It wasn't even close.

I already had a margin-call calculator for regular margin buying (the long side), so when someone asked for a short-selling version I figured it'd be a ten-minute job: take the existing ratio formula, flip a sign so the loss direction points up instead of down, done. It wasn't. Once I actually sat down to write the short-side math, I realized the "maintenance ratio" isn't a single universal equation at all — depending on which market's convention you're modeling, it's two structurally different formulas, and the code has to branch on which one applies before it can even ask "what price triggers a margin call."

Two regions, two different ratio formulas

The tool splits behavior on a locale check:

const isAsianMode = computed(() => {
  return ["tw", "cn", "kr"].includes(locale.value);
});
Enter fullscreen mode Exit fullscreen mode

That one boolean decides which of two completely different maintenance-ratio formulas gets used:

const now_maintenance_ratio = computed(() => {
  if (!now_market_value.value) return 0;

  let totalAsset =
    old_market_value.value + self_provided.value + maintenance_margin.value;

  let res = 0;
  if (isAsianMode.value) {
    // Asia: total collateral / current market value (call below 130%)
    res = totalAsset / now_market_value.value;
  } else {
    // West: (total collateral - current market value) / current market value
    // i.e. equity / liability
    res = (totalAsset - now_market_value.value) / now_market_value.value;
  }

  return Math.floor(res * 100);
});
Enter fullscreen mode Exit fullscreen mode

totalAsset is the same thing in both branches — proceeds from the short sale plus the initial margin you put up plus anything you've added since. What differs is the denominator relationship. The Asian-market version treats the ratio as "how much collateral do I have relative to what I owe," full stop, so it's always comfortably above 100% (the usual floor is 130%). The Western version subtracts the current market value first — it's computing account equity (what's left over after covering your short liability) as a fraction of the liability, which is why 25–40% is a normal maintenance floor there instead of 130%. Same underlying accounting, but they're not the same formula with a constant swapped — one has a subtraction the other doesn't.

That difference isn't cosmetic. It's why the component also switches default thresholds when you change language:

watch(
  locale,
  (newVal) => {
    if (["tw", "cn", "kr"].includes(newVal)) {
      maintenance_ratio.value = 130;
      target_maintenance_ratio.value = 167;
    } else {
      maintenance_ratio.value = 30; // Western-style equity %
      target_maintenance_ratio.value = 50;
    }
  },
  { immediate: true },
);
Enter fullscreen mode Exit fullscreen mode

If you plugged a Taiwan-style 130% floor into the Western formula (or vice versa), the numbers would be nonsense — you'd either never trigger a call or trigger one immediately. The defaults aren't just localization polish; they exist because the underlying math actually changes shape.

Solving for the price, not just the ratio

The more useful number isn't "what's my ratio right now," it's "at what price do I get called." That means solving the ratio equation for price instead of evaluating it. For the Asian branch that's simple algebra:

Asset / Price = Ratio  =>  Price = Asset / Ratio
Enter fullscreen mode Exit fullscreen mode

For the Western branch, the subtraction means an extra step:

(Asset - Price) / Price = Ratio
Asset / Price - 1 = Ratio
Price = Asset / (1 + Ratio)
Enter fullscreen mode Exit fullscreen mode

Both show up almost verbatim in the code, just divided by quantity to turn total asset value back into a per-share price:

const gg_price = computed(() => {
  if (!sell_quantity.value) return 0;

  let totalAsset =
    old_market_value.value + self_provided.value + maintenance_margin.value;
  let ratioDecimal = maintenance_ratio.value / 100;
  let gg = 0;

  if (isAsianMode.value) {
    gg = totalAsset / ratioDecimal / sell_quantity.value;
  } else {
    gg = totalAsset / (1 + ratioDecimal) / sell_quantity.value;
  }

  return Math.ceil(gg * 100) / 100;
});
Enter fullscreen mode Exit fullscreen mode

This is the actual mirror-image part, and it's a genuine inversion, not just a sign flip: in a long margin-call calculator the danger direction is the stock price falling (your collateral shrinks relative to your loan). Here now_market_value — the thing working against you — is now_price * sell_quantity, and it grows as price rises, because rising price means it costs more to buy back the shares you borrowed and sold. Same "collateral vs. liability" skeleton as the long-side tool, opposite trigger direction, and (for the Western branch) an extra subtraction that the long-side formula never needed.

Working out how much cash to inject to get back to a target ratio is the same algebra run in reverse — set the ratio equation equal to a target instead of the current value, then solve for the "add" term:

// Asian: (Asset + Add) / Price = Target  =>  Add = Price * Target - Asset
needValue = now_market_value.value * targetDecimal - totalAsset;
// Western: ((Asset + Add) - Price) / Price = Target  =>  Add = Price * (1 + Target) - Asset
needValue = now_market_value.value * (1 + targetDecimal) - totalAsset;
Enter fullscreen mode Exit fullscreen mode

What this doesn't account for

The feature description says it outright: the calculation excludes fees and taxes. It also doesn't model the cost that's specific to shorting and doesn't exist on the long side at all — the daily stock-borrow fee you pay for as long as the position stays open. Every non-Asian language file mentions this in the intro copy as a caveat, but it's genuinely absent from the computation, not just undocumented; totalAsset never touches a daily accrual.

The other asymmetry is economic, not a code bug, but it's the reason the whole tool exists: a long position's maximum loss is capped (the stock can only fall to zero), while a short position's loss is theoretically unbounded, since price has no ceiling. The calculator will happily compute a "you get called at $X" number, but nothing in the math communicates that the position you're protecting has open-ended downside in a way a long position never does — that's on the person reading the result, not the formula.

One more small thing worth knowing if you go digging in the source yourself: the maintenance-ratio input has a locale-dependent minimum (:min="isAsianMode ? 100 : 0"), which quietly enforces that the Asian-mode floor can never be typed in below 100% — a sensible guard given that formula has no subtraction term to keep it in a sane range on its own.

I turned this into a small free tool if you want to plug in real numbers instead of tracing the algebra yourself: Short Selling Calculator. No sign-up, and if you're on the long side instead, there's a companion margin-call calculator linked from the same page.


Available in other languages

Top comments (0)