from .finance_margin_types import MarginBreakdown from .math_round_div import round_div from .money_add import subtract_money from .money_amount import Money def margin(cost: Money, price: Money) -> MarginBreakdown: """Profit on a sale, expressed both ways. Margin divides by the price, markup divides by the cost, and the gap between them is wide enough to price a whole range wrongly: 10.00 to 15.00 is a 50% markup and a 33.33% margin. Returning only one of them invites the caller to use it as the other, so both are returned and neither is called "the rate". Both ratios are in basis points and come out of integer division, so no float ever touches the money. """ # subtract_money also rejects a GBP cost against a EUR price, which is the # only way this function can be called with genuinely unanswerable inputs. profit = subtract_money(price, cost) # A zero denominator is not an error here: a giveaway (price 0) and a free # sample (cost 0) are real trade. The ratio is undefined, not wrong, and # the rest of the breakdown is still correct. return MarginBreakdown( cost=cost, price=price, profit=profit, margin_basis_points=None if price.minor == 0 else round_div(profit.minor * 10000, price.minor, "half-up"), markup_basis_points=None if cost.minor == 0 else round_div(profit.minor * 10000, cost.minor, "half-up"), ) def margin_to_markup_basis_points(margin_basis_points: int) -> int: """Convert a margin to the markup that produces it. Undefined at 100% margin.""" if margin_basis_points >= 10000: raise ValueError("a margin of %d basis points has no finite markup" % (margin_basis_points,)) return round_div(margin_basis_points * 10000, 10000 - margin_basis_points, "half-up") def markup_to_margin_basis_points(markup_basis_points: int) -> int: """Convert a markup to the margin it produces.""" return round_div(markup_basis_points * 10000, 10000 + markup_basis_points, "half-up")