diff --git a/transaction_processor.py b/transaction_processor.py index 6fc5647..7e161f7 100644 --- a/transaction_processor.py +++ b/transaction_processor.py @@ -704,15 +704,56 @@ class LamassuTransactionProcessor: logger.info("No clients with remaining DCA balance - skipping distribution") return {} - # Calculate proportional distribution + # Calculate proportional distribution with remainder allocation distributions = {} + distributed_sats = 0 + client_calculations = [] + # First pass: calculate base amounts and track remainders for client_id, client_balance in client_balances.items(): # Calculate this client's proportion of the total DCA pool proportion = client_balance / total_confirmed_deposits - # Calculate client's share of the base crypto (after commission) - client_sats_amount = int(base_crypto_atoms * proportion) + # Calculate exact share (with decimals) + exact_share = base_crypto_atoms * proportion + + # Use banker's rounding for base allocation + client_sats_amount = round(exact_share) + + client_calculations.append({ + 'client_id': client_id, + 'proportion': proportion, + 'exact_share': exact_share, + 'allocated_sats': client_sats_amount, + 'client_balance': client_balance + }) + + distributed_sats += client_sats_amount + + # Handle any remainder due to rounding (should be small) + remainder = base_crypto_atoms - distributed_sats + + if remainder != 0: + logger.info(f"Distributing remainder: {remainder} sats among {len(client_calculations)} clients") + + # Sort clients by largest fractional remainder to distribute fairly + client_calculations.sort( + key=lambda x: x['exact_share'] - x['allocated_sats'], + reverse=True + ) + + # Distribute remainder one sat at a time to clients with largest fractional parts + for i in range(abs(remainder)): + if remainder > 0: + client_calculations[i % len(client_calculations)]['allocated_sats'] += 1 + else: + client_calculations[i % len(client_calculations)]['allocated_sats'] -= 1 + + # Second pass: create distributions with final amounts + for calc in client_calculations: + client_id = calc['client_id'] + client_sats_amount = calc['allocated_sats'] + proportion = calc['proportion'] # Calculate equivalent fiat value in centavos for tracking purposes (industry standard) # Store as centavos to maintain precision and avoid floating-point errors @@ -726,6 +767,13 @@ class LamassuTransactionProcessor: logger.info(f"Client {client_id[:8]}... gets {client_sats_amount} sats (≈{client_fiat_amount/100:.2f} GTQ, {proportion:.2%} share)") + # Verification: ensure total distribution equals base amount + total_distributed = sum(dist["sats_amount"] for dist in distributions.values()) + if total_distributed != base_crypto_atoms: + logger.error(f"Distribution mismatch! Expected: {base_crypto_atoms} sats, Distributed: {total_distributed} sats") + raise ValueError(f"Satoshi distribution calculation error: {base_crypto_atoms} != {total_distributed}") + + logger.info(f"Distribution verified: {total_distributed} sats distributed across {len(distributions)} clients") return distributions except Exception as e: