⚡️ Speed up method probit.parse_balance by 28%
#51
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
📄 28% (0.28x) speedup for
probit.parse_balanceinpython/ccxt/async_support/probit.py⏱️ Runtime :
30.2 milliseconds→23.7 milliseconds(best of29runs)📝 Explanation and details
The optimized code achieves a 27% speedup primarily through three key optimizations:
1. Optimized Dictionary Access in safe_string and safe_value
The original code uses
Exchange.key_exists()for every dictionary lookup, which is expensive. The optimized version adds fast-path checks for common dictionary and list types, using direct try/except blocks instead of the generickey_exists()method. This reduces the per-hit time from ~1197ns to ~202ns forsafe_string(83% faster per call).2. Reduced Function Call Overhead in safe_balance
The original version repeatedly calls
self.safe_string(),self.parse_number(), and performs multiplebalance[code]dictionary lookups inside the loop. The optimized version caches these method references and thebalance[code]dictionary as local variables (account_dict), eliminating repeated attribute lookups and dictionary access overhead.3. Method Caching in parse_balance
The optimized version hoists method lookups (
safe_value,safe_string,safe_currency_code,account_method) outside the loop, avoiding repeated attribute resolution for each balance item processed.Performance Impact Analysis
Based on the annotated tests, the optimizations show consistent 10-40% improvements across all test cases, with larger improvements for:
The optimizations are particularly effective for workloads processing many balance entries, as the per-item overhead reduction compounds. Since
parse_balanceis likely called frequently in trading applications for account balance updates, this 27% improvement can significantly impact overall application performance.✅ Correctness verification report:
🌀 Generated Regression Tests and Runtime
import pytest
from ccxt.async_support.probit import probit
Unit tests for probit.parse_balance
@pytest.fixture
def probit_instance():
return probit()
1. Basic Test Cases
def test_single_currency_basic(probit_instance):
# Basic single currency, all fields present
response = {
'data': [
{'currency_id': 'BTC', 'total': '1.5', 'available': '1.0'}
]
}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 30.4μs -> 27.5μs (10.4% faster)
def test_multiple_currencies_basic(probit_instance):
# Multiple currencies, all fields present
response = {
'data': [
{'currency_id': 'BTC', 'total': '2.0', 'available': '1.5'},
{'currency_id': 'ETH', 'total': '10.0', 'available': '7.5'}
]
}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 41.5μs -> 36.9μs (12.5% faster)
def test_currency_code_mapping(probit_instance):
# Test common currency mapping (e.g. XBT -> BTC)
response = {
'data': [
{'currency_id': 'XBT', 'total': '3.0', 'available': '2.0'},
{'currency_id': 'BCHSV', 'total': '1.0', 'available': '0.5'}
]
}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 42.3μs -> 36.8μs (14.8% faster)
def test_info_key_present(probit_instance):
# The 'info' key should always be present and match input
response = {'data': [{'currency_id': 'BTC', 'total': '1', 'available': '1'}]}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 29.0μs -> 25.4μs (14.1% faster)
def test_empty_data(probit_instance):
# No balances
response = {'data': []}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 6.64μs -> 5.41μs (22.8% faster)
2. Edge Test Cases
def test_missing_total_field(probit_instance):
# Missing 'total', should infer from available + used if present
response = {
'data': [
{'currency_id': 'ETH', 'available': '2.5', 'used': '1.5'}
]
}
# Simulate parse_balance to handle 'used' field
# We add 'used' to the account after parse_balance
def patched_parse_balance(response):
result = {
'info': response,
'timestamp': None,
'datetime': None,
}
data = probit_instance.safe_value(response, 'data', [])
for balance in data:
currencyId = probit_instance.safe_string(balance, 'currency_id')
code = probit_instance.safe_currency_code(currencyId)
account = probit_instance.account()
account['free'] = probit_instance.safe_string(balance, 'available')
account['used'] = probit_instance.safe_string(balance, 'used')
account['total'] = None
result[code] = account
return probit_instance.safe_balance(result)
balance = patched_parse_balance(response)
def test_missing_available_field(probit_instance):
# Missing 'available', should infer from total - used
response = {
'data': [
{'currency_id': 'ETH', 'total': '5.0', 'used': '1.5'}
]
}
def patched_parse_balance(response):
result = {
'info': response,
'timestamp': None,
'datetime': None,
}
data = probit_instance.safe_value(response, 'data', [])
for balance in data:
currencyId = probit_instance.safe_string(balance, 'currency_id')
code = probit_instance.safe_currency_code(currencyId)
account = probit_instance.account()
account['total'] = probit_instance.safe_string(balance, 'total')
account['used'] = probit_instance.safe_string(balance, 'used')
account['free'] = None
result[code] = account
return probit_instance.safe_balance(result)
balance = patched_parse_balance(response)
def test_missing_used_field(probit_instance):
# Missing 'used', should infer from total - free
response = {
'data': [
{'currency_id': 'ETH', 'total': '5.0', 'available': '3.5'}
]
}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 31.1μs -> 26.5μs (17.3% faster)
def test_null_fields(probit_instance):
# Fields are None/null
response = {
'data': [
{'currency_id': 'BTC', 'total': None, 'available': None}
]
}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 19.1μs -> 16.2μs (17.7% faster)
def test_zero_balance(probit_instance):
# Zero balances
response = {
'data': [
{'currency_id': 'BTC', 'total': '0', 'available': '0'}
]
}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 29.0μs -> 26.1μs (11.2% faster)
def test_negative_balance(probit_instance):
# Negative balances
response = {
'data': [
{'currency_id': 'BTC', 'total': '-1.5', 'available': '-1.0'}
]
}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 31.5μs -> 27.7μs (13.5% faster)
def test_missing_currency_id(probit_instance):
# Missing currency_id should result in None key
response = {
'data': [
{'total': '1.0', 'available': '1.0'}
]
}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 31.0μs -> 26.9μs (15.2% faster)
def test_extra_fields_ignored(probit_instance):
# Extra fields should be ignored
response = {
'data': [
{'currency_id': 'BTC', 'total': '2.0', 'available': '1.0', 'extra': 'ignored'}
]
}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 32.3μs -> 28.2μs (14.6% faster)
def test_debt_field(probit_instance):
# Debt field should be parsed and present in debt dict
response = {
'data': [
{'currency_id': 'BTC', 'total': '2.0', 'available': '1.0', 'debt': '0.5'}
]
}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 31.6μs -> 27.9μs (13.1% faster)
def test_duplicate_currencies(probit_instance):
# Duplicate currency_id entries should overwrite previous
response = {
'data': [
{'currency_id': 'BTC', 'total': '1.0', 'available': '0.5'},
{'currency_id': 'BTC', 'total': '2.0', 'available': '1.5'}
]
}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 35.0μs -> 30.4μs (15.1% faster)
def test_unexpected_data_type(probit_instance):
# Data is not a list
response = {
'data': None
}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 6.50μs -> 5.39μs (20.5% faster)
def test_empty_response(probit_instance):
# Empty response dict
response = {}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 6.96μs -> 6.02μs (15.7% faster)
3. Large Scale Test Cases
def test_many_currencies_scalability(probit_instance):
# 500 currencies
currencies = []
for i in range(500):
currencies.append({'currency_id': f'C{i}', 'total': str(i), 'available': str(i/2)})
response = {'data': currencies}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 3.85ms -> 3.03ms (27.1% faster)
# Check random samples
for i in [0, 1, 100, 250, 499]:
code = f'C{i}'.upper()
def test_large_balances_and_precision(probit_instance):
# Large numbers and high precision
response = {
'data': [
{'currency_id': 'BTC', 'total': '123456789.123456789', 'available': '123456788.123456789'}
]
}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 34.9μs -> 31.0μs (12.9% faster)
def test_performance_under_load(probit_instance):
# 1000 currencies, check that function does not crash or slow down
currencies = []
for i in range(1000):
currencies.append({'currency_id': f'CUR{i}', 'total': str(i*2), 'available': str(i)})
response = {'data': currencies}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 7.00ms -> 5.64ms (24.2% faster)
# Spot check
for i in [0, 500, 999]:
code = f'CUR{i}'.upper()
def test_sparse_balances(probit_instance):
# Many currencies, some missing fields
currencies = []
for i in range(500):
entry = {'currency_id': f'SPARSE{i}'}
if i % 3 == 0:
entry['total'] = str(i)
if i % 5 == 0:
entry['available'] = str(i/2)
currencies.append(entry)
response = {'data': currencies}
codeflash_output = probit_instance.parse_balance(response); balance = codeflash_output # 2.34ms -> 1.73ms (34.9% faster)
for i in [0, 15, 30, 45, 60, 75, 90, 120, 150, 300, 450]:
code = f'SPARSE{i}'.upper()
# If both total and available present, used should be total - available
total = float(i) if i % 3 == 0 else None
free = float(i/2) if i % 5 == 0 else None
if total is not None and free is not None:
pass
elif total is not None:
pass
elif free is not None:
pass
def test_large_info_field(probit_instance):
# Large info field, should be preserved
info_field = {'data': [{'currency_id': 'BTC', 'total': '1', 'available': '1'}], 'extra': 'x' * 1000}
codeflash_output = probit_instance.parse_balance(info_field); balance = codeflash_output # 29.3μs -> 25.7μs (13.8% faster)
codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
#------------------------------------------------
import pytest
from ccxt.async_support.probit import probit
------------------- UNIT TESTS -------------------
@pytest.fixture
def exchange():
return probit()
1. Basic Test Cases
def test_single_currency_basic(exchange):
# Basic: one currency, all fields present
response = {
'data': [
{'currency_id': 'BTC', 'total': '1.234', 'available': '0.234'}
]
}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 32.7μs -> 29.5μs (11.0% faster)
def test_multiple_currencies_basic(exchange):
# Basic: multiple currencies, all fields present
response = {
'data': [
{'currency_id': 'BTC', 'total': '1.0', 'available': '0.5'},
{'currency_id': 'ETH', 'total': '2.0', 'available': '1.0'}
]
}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 41.4μs -> 37.1μs (11.6% faster)
def test_currency_code_mapping(exchange):
# Basic: currency_id mapping (XBT -> BTC)
response = {
'data': [
{'currency_id': 'XBT', 'total': '1.0', 'available': '0.5'}
]
}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 30.1μs -> 26.3μs (14.5% faster)
def test_empty_data(exchange):
# Basic: empty data list
response = {'data': []}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 6.59μs -> 5.67μs (16.1% faster)
def test_no_data_key(exchange):
# Basic: missing 'data' key (should default to empty)
response = {}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 7.07μs -> 5.99μs (18.1% faster)
2. Edge Test Cases
def test_missing_total(exchange):
# Edge: missing 'total', should compute from available+used
response = {
'data': [
{'currency_id': 'BTC', 'available': '0.1', 'used': '0.2'}
]
}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 20.8μs -> 17.5μs (18.7% faster)
def test_missing_available(exchange):
# Edge: missing 'available', should compute from total-used
response = {
'data': [
{'currency_id': 'ETH', 'total': '2.0', 'used': '1.5'}
]
}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 20.2μs -> 17.1μs (17.6% faster)
def test_missing_used(exchange):
# Edge: missing 'used', should compute from total-free
response = {
'data': [
{'currency_id': 'USDT', 'total': '10.0', 'available': '3.0'}
]
}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 31.9μs -> 28.9μs (10.4% faster)
def test_all_missing(exchange):
# Edge: missing both 'total' and 'used' (should leave as None)
response = {
'data': [
{'currency_id': 'BTC', 'available': '2.0'}
]
}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 19.9μs -> 17.3μs (14.9% faster)
def test_none_values(exchange):
# Edge: None values should be handled gracefully
response = {
'data': [
{'currency_id': 'BTC', 'total': None, 'available': None}
]
}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 19.8μs -> 17.0μs (16.7% faster)
def test_extra_fields_ignored(exchange):
# Edge: extra fields in balance dict should be ignored
response = {
'data': [
{'currency_id': 'BTC', 'total': '1.0', 'available': '0.5', 'foo': 'bar'}
]
}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 30.8μs -> 26.8μs (15.1% faster)
def test_currency_id_none(exchange):
# Edge: currency_id is None
response = {
'data': [
{'currency_id': None, 'total': '1.0', 'available': '0.5'}
]
}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 28.5μs -> 25.2μs (13.2% faster)
def test_debt_field(exchange):
# Edge: debt field is present
response = {
'data': [
{'currency_id': 'BTC', 'total': '5.0', 'available': '4.0', 'debt': '1.0'}
]
}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 31.6μs -> 28.0μs (12.8% faster)
def test_duplicate_currency_ids(exchange):
# Edge: duplicate currency_id entries, last one wins
response = {
'data': [
{'currency_id': 'BTC', 'total': '1.0', 'available': '0.5'},
{'currency_id': 'BTC', 'total': '2.0', 'available': '1.5'}
]
}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 34.7μs -> 30.8μs (12.6% faster)
def test_currency_id_case_insensitive(exchange):
# Edge: currency_id case-insensitivity
response = {
'data': [
{'currency_id': 'btc', 'total': '1.0', 'available': '0.5'}
]
}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 30.1μs -> 26.1μs (15.4% faster)
3. Large Scale Test Cases
def test_many_currencies(exchange):
# Large scale: 1000 currencies, all fields present
num_currencies = 1000
response = {
'data': [
{'currency_id': f'C{i}', 'total': str(i), 'available': str(i / 2)}
for i in range(num_currencies)
]
}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 7.68ms -> 6.18ms (24.4% faster)
for i in range(num_currencies):
code = f'C{i}'.upper()
def test_large_balances(exchange):
# Large scale: very large numbers
response = {
'data': [
{'currency_id': 'BTC', 'total': '1000000000.123456', 'available': '999999999.123456'}
]
}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 35.1μs -> 30.6μs (14.5% faster)
def test_large_data_structure(exchange):
# Large scale: each currency has extra unused fields, should not affect result
num_currencies = 500
response = {
'data': [
{
'currency_id': f'C{i}',
'total': str(i),
'available': str(i / 2),
'unused1': 'foo',
'unused2': 'bar'
}
for i in range(num_currencies)
]
}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 3.89ms -> 3.08ms (26.2% faster)
for i in range(num_currencies):
code = f'C{i}'.upper()
def test_performance_many_balances(exchange):
# Large scale: performance test with 1000 currencies, missing some fields
num_currencies = 1000
response = {
'data': [
{'currency_id': f'C{i}', 'available': str(i)}
for i in range(num_currencies)
]
}
codeflash_output = exchange.parse_balance(response); balance = codeflash_output # 4.54ms -> 3.23ms (40.6% faster)
for i in range(num_currencies):
code = f'C{i}'.upper()
codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
To edit these changes
git checkout codeflash/optimize-probit.parse_balance-mhvmnxe5and push.