This is doable.
The implementation details will turn some assumptions upside down. You have identified the high cost of a brute force approach. This is a valid concern but it can be addressed.
There could be tens of thousands of shareholders and hundreds of thousands of trades. So updating balances for each shareholder on each trade event would be too costly.
Let us assume it's a standard ERC20 contract. There is a balanceOf(address) function that returns a uint. The interface standard is silent on the method of calculation. Given an initial state and a history of all the trades that have taken place it should be possible to compute a shareholder balance as the sum of the initial (stored) state and the dividends received since the state was last updated.
Iteration to process dividends could potentially exceed the block gas limit, so the state needs to be brought up-to-date periodically. In theory, this can be done in batches and the shareholder can be persuaded to pay for the gas required to do so. An opportune time to perform such housekeeping is any time they agree to pay for gas such as when they do a transfer() but there is no prohibition on getting them to trigger it for its own sake from time to time. It can be largely migrated to a UI/UX concern.
As transactions move through the transfer() function, you'll need to keep track of the sums that are vital to computing entitlements. Those will things like the total shares issued at that time (which gives the % for any given shareholder). You'll move the 1% into an administrative account.
- Alice has 100 shares.
- $1,000 was transferred from Bob to Carol, while 1,000 shares were issued.
- Given the rule, Bob is debited 1,000 and Carol receives 9,990. 10 go in the admin account.
- This is an opportunity to perform housekeeping on both Bob and Carol, and Bob might be persuaded to bang on the contract as many times as it takes to clean house so the system will permit the transaction to go through. Consider a new function (it's allowed) to perform
housekeeping(address account) - The housekeeping function would move funds held in the admin account to the shareholder. It always exactly equals the unprocessed distributions to all shareholders who's stored balances are always a little bit behind, and to different degrees.
Keep in mind that read-only operations do not cost gas so as long as they complete within the gasLimit, the system can chug along and correctly report shareholder balances without updated account states.
A function that computes the net position of an account would crawl the unprocessed transactions to compute it. You would embed it in the balanceOf() function and arrange things so that if someone (anyone) is paying for gas, then store the result.
You will be able to optimize the process by grouping both the transactions and the shareholders. For example, shareholders commence receiving starting at the next cutoff time rather than immediately. Transactions grouped into periods like, say, 24 hours, would mean the same process could be applied to daily summaries rather than individual transactions, and this would reduce the number of iterations required, which is what you want. In case that isn't clear, it's the difference between "Alice is 23,000 transactions behind" and Alice is "7 days behind."
Importantly, every time Alice sends or receives, someone pays for the gas to update her stored balance so she is no longer behind. The idea here is to make it a rare case when any sort of manual intervention is needed. Obviously, Alice (and others) need a possibility to catch her up in steps in the case that she is so far behind it can no longer be computed in a single transaction.
You might think about minimum transaction size where these calculations should even apply, the appropriate duration of the accounting periods, the rate and whether those parameters should be changeable over time, and by whom. I'm not sure this ad-hoc description adequately explains the on-the-fly balance computations but hopefully it gives you a new way to consider it.
Hope it helps.