How cardwise computes things

1. Exact simulation (engine.simulate)

Transactions of one card are replayed in date order. For each one:

  1. If its category is excluded: base earn is 0, no rule applies (the fuel surcharge waiver still can).
  2. Base earn = earn_units(amount, base_rate) with spend rounding and point rounding.
  3. For each matching rule, bonus = rule earn - base earn. The rule's cap bucket (rule id, period bucket of the date) has some allowance left; the usable bonus is min(bonus, allowance). The rule with the largest usable bonus wins and consumes allowance. What does not fit is reported as "cap loss".
  4. Fuel surcharge (if in range) is added, capped per period bucket in rupees.
  5. Eligible spend feeds each milestone's (milestone, period bucket) counter. The first transaction that takes a counter to its target carries the reward.
  6. Value in rupees = (base + bonus + milestone points) * point_value + surcharge + milestone cash.

Worked example (Dining Delight: base 2 pts/Rs 100, dining 10 pts/Rs 100, cap 1,000 bonus points per cycle, 1 point = Rs 0.25). A Rs 6,000 dinner earns 120 base points and 600 at the rule rate, so its bonus is 480. In one cycle the first dinner earns 480 bonus points and the second 480 (960 used). The third finds only 40 left: it earns 40 and reports 440 points (Rs 110) of cap loss.

Cycle bucket: with statement_day: 15, 15 Jan is in the cycle ending 15 Jan and 16 Jan in the cycle ending 15 Feb.

Annual fee: the fee (with GST) is waived when spend not in fee_waiver.exclude reaches the threshold within the calendar year.

2. Optimal assignment (optimizer)

Question: given every transaction of the year, which card should each have been put on to maximise total value, when caps, milestones and fee waivers couple the transactions?

Greedy per-transaction choice fails because caps are shared. Example: card A pays 10% on dining up to Rs 100 bonus per month, card B pays 5% flat. Three Rs 1,000 dinners: independent best-card-per-transaction puts all three on A (cap exhausted after one); the optimum is one on A and two on B.

It is solved as a mixed-integer linear program with SciPy's HiGHS:

After solving, the assignment is replayed through the exact simulator, and that number is what is reported. Moves that gain nothing (many equal-value solutions exist, e.g. rent that no card rewards) are reverted greedily, and if the simulated optimum were ever below what you actually earned, your actual assignment is kept. The model lets a transaction use any matching rule while the simulator chooses the best one available in date order, so the two agree except in rare cases of several overlapping capped rules.

HiGHS runs with a relative gap of 1e-4 (about Rs 3 on Rs 30,000), which turns a 25 second proof of optimality into under a second on a year of data. Tests compare the result against brute force over all assignments on small random instances with caps, milestones, rounding and fees.

Assumption: any card can be used at any merchant.

3. Policy derivation (policy)

A per-transaction plan is not actionable. The policy converts the optimal assignment into rules of the form "use A for the first Rs X per month in this category, then B":

  1. For each category and card, take the monthly spend the optimum put there.
  2. Drop cards with under 5% of the category's spend.
  3. Order cards by effective rate in that category (milestone rewards excluded, because they are not earned per rupee).
  4. A card's monthly limit is the median of its non-zero months, rounded to Rs 100. The last card takes the overflow.

The policy is then replayed on the real transactions month by month, and the earned value is printed next to your actual and the theoretical optimum, so you can see how much of the gain a simple rule retains. Milestone-driven moves are not captured as limits, so the policy usually lands slightly below the optimum.

Example from the sample data: online shopping is "cashback-plus for the first Rs 23,300/month, then travel-elite". The 5% rule's Rs 1,000 cycle cap is used up at about Rs 25,000 of spend. Past that, cashback-plus would still pay its 1% base, but the optimum sends the overflow to travel-elite because that spend is what lets travel-elite reach its quarterly and annual milestones.

4. Fee worth-it (fee-check)

For each card c:

net benefit = rewards on c in the optimum
            - fee payable on c (0 if the optimum reaches the waiver threshold)
            - foregone elsewhere
            + extras (welcome benefit with --first-year, lounge value with --lounge-visits)
foregone    = optimum(without c) - (optimum(with c) - net of c)

Optima are net of fees on all cards, so the fee-waiver effect is part of every number. KEEP if net benefit is at least 25% of the fee, MARGINAL if positive but below, DROP if negative, FREE for no-fee cards that do no harm.

5. Milestone pacing (policy)

For each unfinished milestone in the period containing --as-of:

6. Prospective cards (simulate)

A spend profile (monthly rupees per category) is expanded into 12 months of synthetic transactions (count from avg_txn, default Rs 2,000). Standalone value is the optimum with only that card. With --alongside, marginal benefit is the optimum with the held cards plus the candidate, minus the optimum with the held cards alone, both net of fees.