Decimal Precision Configuration Guide¶
Overview¶
RustyBT provides a flexible decimal precision configuration system that allows different asset classes (cryptocurrencies, equities, forex, futures) to use appropriate precision based on their specific requirements and data provider specifications.
Why Decimal Precision Matters¶
Financial calculations require precise arithmetic to avoid rounding errors that accumulate over many operations. Python's float type uses binary floating-point representation, which can introduce subtle errors in decimal calculations:
# Float precision issues
>>> 0.1 + 0.2
0.30000000000000004 # Not exactly 0.3!
# Decimal precision
>>> from decimal import Decimal
>>> Decimal("0.1") + Decimal("0.2")
Decimal('0.3') # Exact!
RustyBT uses Python's decimal module throughout the financial calculation stack to ensure audit-compliant precision.
Configuration Schema¶
YAML Configuration¶
The decimal precision system is configured via YAML files with the following structure:
# Global defaults
global_defaults:
precision: 18 # Total significant digits
rounding_mode: ROUND_HALF_EVEN # Banker's rounding
scale: 8 # Decimal places for display
# Asset class-specific settings
asset_classes:
crypto:
precision: 18
rounding_mode: ROUND_DOWN
scale: 8
rationale: "Cryptocurrencies require high precision for fractional shares"
equity:
precision: 18
rounding_mode: ROUND_HALF_UP
scale: 2
rationale: "Equities trade in cents"
# ... more asset classes
Configuration Parameters¶
| Parameter | Type | Range | Description |
|---|---|---|---|
precision |
int | 0-18 | Total number of significant digits |
rounding_mode |
str | See below | Rounding algorithm to use |
scale |
int | 0-18 | Decimal places for display/formatting |
Default Precision Presets¶
RustyBT provides sensible defaults for common asset classes:
Cryptocurrency (crypto)¶
- Precision: 18 digits
- Rounding Mode: ROUND_DOWN
- Scale: 8 decimal places
- Rationale: Bitcoin uses Satoshi precision (0.00000001 BTC = 1 Satoshi). High precision ensures accurate calculations for fractional cryptocurrency holdings.
Equities (equity)¶
- Precision: 18 digits
- Rounding Mode: ROUND_HALF_UP
- Scale: 2 decimal places
- Rationale: US stocks trade in cents ($42.50), but large positions require high precision for intermediate calculations.
Forex (forex)¶
- Precision: 18 digits
- Rounding Mode: ROUND_HALF_EVEN
- Scale: 5 decimal places
- Rationale: Forex pairs require pip precision (0.00001 for most pairs, 0.001 for JPY pairs).
Futures (future)¶
- Precision: 18 digits
- Rounding Mode: ROUND_HALF_UP
- Scale: 2 decimal places
- Rationale: Tick sizes vary by contract, but 2 decimals covers most cases.
Index (index)¶
- Precision: 18 digits
- Rounding Mode: ROUND_HALF_EVEN
- Scale: 2 decimal places
- Rationale: Index values like S&P 500 typically use 2 decimals (4521.25).
Rounding Modes¶
RustyBT supports all Python decimal rounding modes:
| Mode | Description | Use Case |
|---|---|---|
ROUND_HALF_EVEN |
Round to nearest even (Banker's rounding) | Default, minimizes bias |
ROUND_DOWN |
Round toward zero | Crypto exchanges (round quantities down) |
ROUND_HALF_UP |
Traditional rounding (0.5 → 1) | Equities, futures |
ROUND_UP |
Round away from zero | Conservative risk calculations |
ROUND_CEILING |
Round toward +∞ | Special cases |
ROUND_FLOOR |
Round toward -∞ | Special cases |
ROUND_05UP |
Round away if last digit is 0 or 5 | Specialized accounting |
Rounding Mode Selection Guide¶
-
ROUND_HALF_EVEN (Banker's Rounding): Best default choice. Over many operations, rounding 0.5 to the nearest even number minimizes cumulative bias.
-
ROUND_DOWN: Use when the exchange or broker always rounds quantities down (common in crypto exchanges).
-
ROUND_HALF_UP: Traditional rounding familiar to most users. Suitable for equities and futures.
-
ROUND_UP: Use for conservative risk calculations where you want to round losses up and gains down.
Usage Examples¶
Basic Configuration Loading¶
from rustybt.finance.decimal import DecimalConfig
# Get singleton instance (loads default config)
config = DecimalConfig.get_instance()
# Check precision for crypto
precision = config.get_precision("crypto") # 18
rounding = config.get_rounding_mode("crypto") # "ROUND_DOWN"
scale = config.get_scale("crypto") # 8
Loading Custom Configuration¶
from rustybt.finance.decimal import DecimalConfig
config = DecimalConfig.get_instance()
# Load from YAML file
config.load_from_yaml("my_custom_config.yaml")
# Load from JSON file
config.load_from_json("config.json")
# Load from dictionary
custom_config = {
"global_defaults": {
"precision": 18,
"rounding_mode": "ROUND_HALF_EVEN",
"scale": 8
},
"asset_classes": {
"crypto": {
"precision": 18,
"rounding_mode": "ROUND_DOWN",
"scale": 8
}
}
}
config.load_from_dict(custom_config)
Programmatic Configuration¶
from rustybt.finance.decimal import DecimalConfig
config = DecimalConfig.get_instance()
# Set custom precision for a new asset class
config.set_precision(
asset_class="option",
precision=18,
rounding_mode="ROUND_HALF_EVEN",
scale=4
)
Using Context Managers for Calculations¶
from decimal import Decimal
from rustybt.finance.decimal import DecimalConfig
config = DecimalConfig.get_instance()
# Calculate with crypto precision
with config.with_precision("crypto") as ctx:
btc_price = Decimal("42150.12345678")
btc_amount = Decimal("0.00012345")
# Calculation uses crypto precision (18 digits, ROUND_DOWN)
total_value = btc_price * btc_amount
print(f"Total: {total_value}") # Precise to 8 decimals
# Calculate with equity precision
with config.with_precision("equity") as ctx:
stock_price = Decimal("150.25")
shares = Decimal("100")
# Calculation uses equity precision (18 digits, ROUND_HALF_UP)
total_value = stock_price * shares
print(f"Total: ${total_value}") # Result: $15025.00
Thread-Safe Context Management¶
The precision system is thread-safe using Python's decimal.localcontext():
import threading
from decimal import Decimal
from rustybt.finance.decimal import DecimalConfig
config = DecimalConfig.get_instance()
def calculate_crypto_value():
"""Thread 1: Calculate crypto position."""
with config.with_precision("crypto") as ctx:
# This context is isolated to this thread
value = Decimal("1.12345678") * Decimal("42000")
return value
def calculate_equity_value():
"""Thread 2: Calculate equity position."""
with config.with_precision("equity") as ctx:
# Different context, isolated from Thread 1
value = Decimal("150.25") * Decimal("100")
return value
# Spawn threads - each has isolated context
thread1 = threading.Thread(target=calculate_crypto_value)
thread2 = threading.Thread(target=calculate_equity_value)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
Advanced Configuration¶
Creating Custom Asset Classes¶
# custom_config.yaml
asset_classes:
# Custom asset class for options
option:
precision: 18
rounding_mode: ROUND_HALF_EVEN
scale: 4
rationale: "Options require 4 decimal precision for Greeks calculations"
# Custom asset class for bonds
bond:
precision: 18
rounding_mode: ROUND_HALF_UP
scale: 6
rationale: "Bonds require high precision for yield calculations"
Configuration File Search Path¶
RustyBT searches for configuration files in the following order:
- User-provided file path (via
load_from_yaml()orload_from_json()) - Project-level config:
./decimal_config.yaml - Package default:
rustybt/finance/decimal/default_config.yaml
Validation and Error Handling¶
The configuration system validates all inputs:
from rustybt.finance.decimal import (
DecimalConfig,
InvalidPrecisionError,
InvalidRoundingModeError,
InvalidAssetClassError
)
config = DecimalConfig.get_instance()
# Invalid precision (out of range 0-18)
try:
config.set_precision("crypto", 50, "ROUND_HALF_EVEN")
except InvalidPrecisionError as e:
print(f"Error: {e}") # Precision must be 0-18
# Invalid rounding mode
try:
config.set_precision("crypto", 8, "ROUND_INVALID")
except InvalidRoundingModeError as e:
print(f"Error: {e}") # Invalid rounding mode
# Unknown asset class
try:
precision = config.get_precision("unknown_asset")
except InvalidAssetClassError as e:
print(f"Error: {e}") # Unknown asset class
Best Practices¶
1. Use High Precision for Intermediate Calculations¶
Always use precision 18 for intermediate calculations, even if final display only needs 2 decimals:
from decimal import Decimal
from rustybt.finance.decimal import DecimalConfig
config = DecimalConfig.get_instance()
# GOOD: High precision for calculation, format for display
with config.with_precision("equity") as ctx:
price = Decimal("150.256789") # High precision input
shares = Decimal("1000")
total = price * shares # Precise calculation
# Format for display with 2 decimals
scale = config.get_scale("equity")
display_value = round(total, scale)
print(f"${display_value}") # $150256.79
# BAD: Low precision loses accuracy
price_low_prec = Decimal("150.26") # Truncated input
total_low_prec = price_low_prec * Decimal("1000")
# Result: $150260.00 (lost precision!)
2. Choose Rounding Mode Based on Use Case¶
- Calculations: Use
ROUND_HALF_EVEN(minimizes bias) - Order Quantities (Crypto): Use
ROUND_DOWN(matches exchange behavior) - Risk Metrics: Use
ROUND_UP(conservative) - Display: Use asset class default
3. Always Use String Construction for Decimal¶
from decimal import Decimal
# GOOD: String construction (exact)
price = Decimal("42.50")
# BAD: Float construction (introduces rounding error)
price = Decimal(42.50) # May not be exactly 42.50!
4. Validate Configuration on Startup¶
from rustybt.finance.decimal import DecimalConfig
config = DecimalConfig.get_instance()
# Load custom config
config.load_from_yaml("my_config.yaml")
# Validate configuration
try:
config.validate_config()
print("Configuration valid ✓")
except Exception as e:
print(f"Configuration invalid: {e}")
exit(1)
Performance Considerations¶
Precision vs Performance¶
Higher precision incurs computational cost. RustyBT defaults to precision 18 as a balance:
- Precision 28 (Python Decimal default): Maximum accuracy, ~50% slower
- Precision 18 (RustyBT default): Excellent accuracy, good performance
- Precision 8: Faster, but may lose accuracy in intermediate calculations
For most use cases, precision 18 provides sufficient accuracy with acceptable performance.
Optimization Tips¶
- Reuse contexts: Use
with_precision()for blocks of calculations, not individual operations - Batch operations: Process multiple calculations within a single context
- Profile first: Only reduce precision if profiling shows it's a bottleneck
Troubleshooting¶
Common Issues¶
Problem: "Unknown asset class" error
Solution: Check that asset class exists in configuration:
config = DecimalConfig.get_instance()
print(config._config["asset_classes"].keys()) # List available classes
Problem: Unexpected rounding behavior
Solution: Verify rounding mode for asset class:
config = DecimalConfig.get_instance()
mode = config.get_rounding_mode("crypto")
print(f"Crypto rounding mode: {mode}")
Problem: Thread context leakage
Solution: Always use with_precision() context manager, never modify global context directly.
Summary¶
- RustyBT uses
decimal.Decimalfor all financial calculations - Precision is configurable per asset class (default: 18 digits)
- Rounding modes support different calculation requirements
- Thread-safe context management via
with_precision() - Defaults provided for crypto, equity, forex, futures, index
- Validation ensures precision (0-18) and rounding modes are valid
For more information, see: - Python Decimal Module Documentation - Examples & Tutorials