O que você está fazendo essencialmente é ignorar o ORM para otimizar o desempenho. Portanto, não se surpreenda por você estar "replicando o trabalho que o ORM está fazendo", porque é exatamente isso que você precisa fazer.
A menos que você tenha muitos lugares onde você precisa fazer atualizações em massa como esta, eu recomendaria contra a abordagem de eventos mágicos; simplesmente escrever as consultas explícitas é muito mais direto.
O que eu recomendo fazer é usar o SQLAlchemy Core ao invés do ORM para fazer a atualização:
ledger = Table("ledger", db.metadata,
Column("wallet_id", Integer, primary_key=True),
Column("new_balance", Float),
prefixes=["TEMPORARY"],
)
wallets = db_session.query(Wallet).all()
# figure out new balances
balance_map = {}
for w in wallets:
balance_map[w.id] = calculate_new_balance(w)
# create temp table with balances we need to update
ledger.create(bind=db.session.get_bind())
# insert update data
db.session.execute(ledger.insert().values([{"wallet_id": k, "new_balance": v}
for k, v in balance_map.items()])
# perform update
db.session.execute(Wallet.__table__
.update()
.values(balance=ledger.c.new_balance)
.where(Wallet.__table__.c.id == ledger.c.wallet_id))
# drop temp table
ledger.drop(bind=db.session.get_bind())
# commit changes
db.session.commit()