Part of a series on building Balance , a portfolio rebalancing app, as a solo developer. Balance started as a Brazilian-only app: B3 tickers, prices in reais, Brazilian income tax. Then came US stocks. Then crypto. The naïve path would have been three apps glued together — three price fetchers, three sets of forms, three rebalancing engines. Instead, the whole thing pivots on a single field: class Portfolio ( BaseModel ): market = models . CharField ( max_length = 6 , choices = [( ' BR ' , ' Brasil ' ), ( ' US ' , ' Estados Unidos ' ), ( ' CRYPTO ' , ' Cripto ' )], default = ' BR ' , ) market is the one axis of variation. Everything market-specific branches on it — and crucially, the rebalancing engine itself doesn't. Let me show where the branches live and why the core stays untouched. Where the code actually differs Three markets differ in four concrete places: Concern BR US Crypto Price source BRAPI → Yahoo ( .SA ) Yahoo (direct) Binance Currency symbol R ' self . fields [ ' asset_class ' ]. choices = STOCK_ETF_ONLY self . fields [ ' current_price ' ]. label = ' Price () ' elif market == ' CRYPTO ' : self . _ticker_re = r ' ^[A-Z]{2,10} ' self . fields [ ' quantity ' ]. widget . attrs [ ' step ' ] = ' 0.00000001 ' else : # BR self . _ticker_re = r ' ^[A-Z]{3,6}\d{0,2}$ ' Same ModelForm , three personalities. The view just passes market=portfolio.market . Branch 3: precision (the sharp edge) This is the one that bites. A stock quantity of 2 is fine in decimal_places=2 . A crypto quantity of 0.00123456 silently rounds to 0.00 . The fix was to widen the precision everywhere money or quantity flows — Asset.quantity , current_price , avg_cost , MarketPrice.price , Transaction fields — all to max_digits=20, decimal_places=8 . But that would over-format stocks ("2.00000000 shares"). So the display branches, while the storage doesn't: {% if is_crypto %} {{ asset.quantity | floatformat : "-8" }} {# 0.00123456 #} {% else %} {{ asset.quantity | floatformat : "0" }} {# 2 #} {% endif %} floatformat:"-8" strips trailing zeros, so 1.50000000 shows as 1.5 . Store wide, render narrow. Branch 4: the rebalancing engine almost doesn't branch I said the engine is shared — that's 95% true. The one place it peeks at market is where whole-share assumptions break: if self . portfolio . market == ' CRYPTO ' : quantity = ( budget / price ). quantize ( Decimal ( ' 0.00000001 ' )) # fraction else : quantity = int ( budget / price ) # whole units That's it. Two or three of these guards across the whole service. Everything else — gap calculation, budget distribution, leftover spending — is market-agnostic because it works in money , and money is the same shape in every market. The trade-off I'd flag The honest downside: if market == 'CRYPTO' sprinkled across the codebase is a smell that's tolerable at three markets and would rot at ten. If a fourth market with genuinely different mechanics showed up (say, options), I'd refactor those branches into a strategy object — market.quantize(budget, price) — rather than keep growing the conditionals. But premature abstraction has its own cost. At three markets, the branches are few, local, and obvious. Designing a pluggable "market backend" architecture on day one would have been more code to serve a flexibility I didn't yet need. The principle that held up: model the axis of variation explicitly ( market ), branch where behavior genuinely differs, and keep the shared core working in the most general currency you have — money. Balance runs Brazilian, US and crypto portfolios through one rebalancing engine, with a consolidated dashboard that converts everything to a single currency. Link in profile. How do you handle multi-tenant-style variation in your apps — branching or strategy objects? Curious in the comments.

Modeling three markets (BR / US / Crypto) with one rebalancing engine
Diego
