Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion investing_algorithm_framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
PortfolioConfiguration, RESOURCE_DIRECTORY, AWS_LAMBDA_LOGGING_CONFIG, \
Trade, APP_MODE, AppMode, DATETIME_FORMAT, load_backtests_from_directory, \
BacktestDateRange, convert_polars_to_pandas, BacktestRun, \
DEFAULT_LOGGING_CONFIG, DataType, DataProvider, StopLossRule, ScalingRule, TradingCost, \
DEFAULT_LOGGING_CONFIG, DataType, DataProvider, StopLossRule, \
ScalingRule, TradingCost, \
TradeStatus, generate_backtest_summary_metrics, generate_algorithm_id, \
APPLICATION_DIRECTORY, DataSource, OrderExecutor, PortfolioProvider, \
SnapshotInterval, AWS_S3_STATE_BUCKET_NAME, BacktestEvaluationFocus, \
Expand Down
5 changes: 4 additions & 1 deletion investing_algorithm_framework/cli/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2297,7 +2297,10 @@ def main(directory: Optional[Union[str, List[str]]] = None):
),
)
args = parser.parse_args()
dirs = args.directory or ([directory] if isinstance(directory, str) else directory) or []
dirs = args.directory or (
[directory] if isinstance(directory, str)
else directory
) or []

if len(dirs) == 1:
server = BacktestMCPServer(dirs[0])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ def __init__(
if isinstance(scale_in_percentage, (int, float)):
self._scale_in_percentages = [float(scale_in_percentage)]
else:
self._scale_in_percentages = [float(v) for v in scale_in_percentage]
self._scale_in_percentages = [
float(v) for v in scale_in_percentage
]

if isinstance(scale_out_percentage, (int, float)):
self._scale_out_percentages = [float(scale_out_percentage)]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from .sql_alchemy import Session, setup_sqlalchemy, SQLBaseModel, \
create_all_tables, clear_db
create_all_tables, clear_db, SqliteDecimal

__all__ = [
"Session",
"setup_sqlalchemy",
"SQLBaseModel",
"create_all_tables",
"clear_db"
"clear_db",
"SqliteDecimal"
]
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import logging
from decimal import Decimal

from sqlalchemy import create_engine, StaticPool
from sqlalchemy import create_engine, StaticPool, String
from sqlalchemy import inspect
from sqlalchemy.orm import DeclarativeBase, sessionmaker
from sqlalchemy import TypeDecorator

from investing_algorithm_framework.domain import SQLALCHEMY_DATABASE_URI, \
OperationalException
Expand All @@ -11,6 +13,39 @@
logger = logging.getLogger("investing_algorithm_framework")


class SqliteDecimal(TypeDecorator):
"""
A type that stores Python numeric values as TEXT in SQLite for
exact precision. This avoids the lossy float conversion that
occurs with SQLAlchemy's Float/Numeric types on SQLite.

- On write: converts the value to its full-precision string
representation via Decimal, then stores as TEXT.
- On read: returns a Python float for backward compatibility
with existing arithmetic code.

The key benefit is lossless **storage**: the TEXT column preserves
the exact decimal representation. For example, a value like
12345678901234.567890123456789 is stored as that exact string
rather than being silently truncated to a 64-bit float.

Use this instead of Column(Float) for monetary values, balances,
prices, and amounts where storage precision matters.
"""
impl = String
cache_ok = True

def process_bind_param(self, value, dialect):
if value is not None:
return str(Decimal(str(value)))
return None

def process_result_value(self, value, dialect):
if value is not None:
return float(Decimal(value))
return None


class SQLAlchemyAdapter:

def __init__(self, app):
Expand Down
20 changes: 11 additions & 9 deletions investing_algorithm_framework/infrastructure/models/order/order.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import logging
from datetime import datetime, timezone

from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Float
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship

from investing_algorithm_framework.domain import OrderType, \
OrderSide, Order, OrderStatus
from investing_algorithm_framework.infrastructure.database import SQLBaseModel
from investing_algorithm_framework.infrastructure.database import (
SQLBaseModel, SqliteDecimal
)
from investing_algorithm_framework.infrastructure.models.model_extension \
import SQLAlchemyModelExtension
from investing_algorithm_framework.infrastructure.models.\
Expand All @@ -33,21 +35,21 @@ class SQLOrder(Order, SQLBaseModel, SQLAlchemyModelExtension):
trades = relationship(
'SQLTrade', secondary=order_trade_association, back_populates='orders'
)
price = Column(Float)
amount = Column(Float)
remaining = Column(Float, default=None)
filled = Column(Float, default=None)
cost = Column(Float, default=0)
price = Column(SqliteDecimal())
amount = Column(SqliteDecimal())
remaining = Column(SqliteDecimal(), default=None)
filled = Column(SqliteDecimal(), default=None)
cost = Column(SqliteDecimal(), default=0)
status = Column(String, default=OrderStatus.CREATED.value)
position_id = Column(Integer, ForeignKey('positions.id'))
position = relationship("SQLPosition", back_populates="orders")
created_at = Column(DateTime(timezone=True), default=utcnow)
updated_at = Column(
DateTime(timezone=True), default=utcnow, onupdate=utcnow
)
order_fee = Column(Float, default=None)
order_fee = Column(SqliteDecimal(), default=None)
order_fee_currency = Column(String, default=None)
order_fee_rate = Column(Float, default=None)
order_fee_rate = Column(SqliteDecimal(), default=None)
sell_order_metadata_id = Column(Integer, ForeignKey('orders.id'))
trade_allocations = relationship(
'SQLTradeAllocation', back_populates='order'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import logging

from sqlalchemy import Column, Integer, ForeignKey, Float
from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship

from investing_algorithm_framework.infrastructure.database import SQLBaseModel
from investing_algorithm_framework.infrastructure.database import (
SQLBaseModel, SqliteDecimal
)
from investing_algorithm_framework.infrastructure.models.model_extension \
import SQLAlchemyModelExtension

Expand Down Expand Up @@ -55,13 +57,13 @@ class SQLTradeAllocation(SQLBaseModel, SQLAlchemyModelExtension):
trade_id = Column(Integer)
stop_loss_id = Column(Integer)
take_profit_id = Column(Integer)
amount = Column(Float)
amount_pending = Column(Float)
open_price = Column(Float, default=0)
close_price = Column(Float, default=0)
buy_fee = Column(Float, default=0)
sell_fee = Column(Float, default=0)
net_gain_contribution = Column(Float, default=0)
amount = Column(SqliteDecimal())
amount_pending = Column(SqliteDecimal())
open_price = Column(SqliteDecimal(), default=0)
close_price = Column(SqliteDecimal(), default=0)
buy_fee = Column(SqliteDecimal(), default=0)
sell_fee = Column(SqliteDecimal(), default=0)
net_gain_contribution = Column(SqliteDecimal(), default=0)

def __init__(
self,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from sqlalchemy import Column, Integer, String, DateTime, Float
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import relationship

from investing_algorithm_framework.domain import PortfolioSnapshot
from investing_algorithm_framework.infrastructure.database import SQLBaseModel
from investing_algorithm_framework.infrastructure.database import (
SQLBaseModel, SqliteDecimal
)
from investing_algorithm_framework.infrastructure.models.model_extension \
import SQLAlchemyModelExtension

Expand All @@ -20,14 +22,14 @@ class SQLPortfolioSnapshot(
id = Column(Integer, primary_key=True)
portfolio_id = Column(String, nullable=False)
trading_symbol = Column(String, nullable=False)
pending_value = Column(Float, nullable=False, default=0)
unallocated = Column(Float, nullable=False, default=0)
net_size = Column(Float, nullable=False, default=0)
total_net_gain = Column(Float, nullable=False, default=0)
total_revenue = Column(Float, nullable=False, default=0)
total_cost = Column(Float, nullable=False, default=0)
total_value = Column(Float, nullable=False, default=0)
cash_flow = Column(Float, nullable=False, default=0)
pending_value = Column(SqliteDecimal(), nullable=False, default=0)
unallocated = Column(SqliteDecimal(), nullable=False, default=0)
net_size = Column(SqliteDecimal(), nullable=False, default=0)
total_net_gain = Column(SqliteDecimal(), nullable=False, default=0)
total_revenue = Column(SqliteDecimal(), nullable=False, default=0)
total_cost = Column(SqliteDecimal(), nullable=False, default=0)
total_value = Column(SqliteDecimal(), nullable=False, default=0)
cash_flow = Column(SqliteDecimal(), nullable=False, default=0)
created_at = Column(DateTime, nullable=False, default=0)
position_snapshots = relationship(
"SQLPositionSnapshot",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from datetime import datetime, timezone

from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean
from sqlalchemy import Column, Integer, String, DateTime, Boolean
from sqlalchemy import UniqueConstraint
from sqlalchemy.orm import relationship
from sqlalchemy.orm import validates

from investing_algorithm_framework.domain import Portfolio
from investing_algorithm_framework.infrastructure.database import SQLBaseModel
from investing_algorithm_framework.infrastructure.database import (
SQLBaseModel, SqliteDecimal
)
from investing_algorithm_framework.infrastructure.models.model_extension \
import SQLAlchemyModelExtension

Expand All @@ -16,14 +18,14 @@ class SQLPortfolio(Portfolio, SQLBaseModel, SQLAlchemyModelExtension):
id = Column(Integer, primary_key=True)
identifier = Column(String, nullable=False, unique=True)
trading_symbol = Column(String, nullable=False)
realized = Column(Float, nullable=False, default=0)
total_revenue = Column(Float, nullable=False, default=0)
total_cost = Column(Float, nullable=False, default=0)
total_net_gain = Column(Float, nullable=False, default=0)
total_trade_volume = Column(Float, nullable=False, default=0)
net_size = Column(Float, nullable=False, default=0)
unallocated = Column(Float, nullable=False, default=0)
initial_balance = Column(Float, nullable=True)
realized = Column(SqliteDecimal(), nullable=False, default=0)
total_revenue = Column(SqliteDecimal(), nullable=False, default=0)
total_cost = Column(SqliteDecimal(), nullable=False, default=0)
total_net_gain = Column(SqliteDecimal(), nullable=False, default=0)
total_trade_volume = Column(SqliteDecimal(), nullable=False, default=0)
net_size = Column(SqliteDecimal(), nullable=False, default=0)
unallocated = Column(SqliteDecimal(), nullable=False, default=0)
initial_balance = Column(SqliteDecimal(), nullable=True)
market = Column(String, nullable=False)
positions = relationship(
"SQLPosition",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Float
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy import UniqueConstraint
from sqlalchemy.orm import relationship, validates

from investing_algorithm_framework.domain import Position
from investing_algorithm_framework.infrastructure.database import SQLBaseModel
from investing_algorithm_framework.infrastructure.database import (
SQLBaseModel, SqliteDecimal
)
from investing_algorithm_framework.infrastructure.models.model_extension \
import SQLAlchemyModelExtension

Expand All @@ -12,8 +14,8 @@ class SQLPosition(SQLBaseModel, Position, SQLAlchemyModelExtension):
__tablename__ = "positions"
id = Column(Integer, primary_key=True, unique=True)
symbol = Column(String)
amount = Column(Float)
cost = Column(Float)
amount = Column(SqliteDecimal())
cost = Column(SqliteDecimal())
orders = relationship(
"SQLOrder",
back_populates="position",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Float
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship

from investing_algorithm_framework.domain import PositionSnapshot
from investing_algorithm_framework.infrastructure.database import SQLBaseModel
from investing_algorithm_framework.infrastructure.database import (
SQLBaseModel, SqliteDecimal
)
from investing_algorithm_framework.infrastructure.models.model_extension \
import SQLAlchemyModelExtension

Expand All @@ -13,8 +15,8 @@ class SQLPositionSnapshot(
__tablename__ = "position_snapshots"
id = Column(Integer, primary_key=True, unique=True)
symbol = Column(String)
amount = Column(Float)
cost = Column(Float)
amount = Column(SqliteDecimal())
cost = Column(SqliteDecimal())
portfolio_snapshot_id = Column(
Integer, ForeignKey('portfolio_snapshots.id')
)
Expand Down
24 changes: 13 additions & 11 deletions investing_algorithm_framework/infrastructure/models/trades/trade.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from sqlalchemy import Column, Integer, String, DateTime, Float
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import relationship

from investing_algorithm_framework.domain import Trade, TradeStatus
from investing_algorithm_framework.infrastructure.database import SQLBaseModel
from investing_algorithm_framework.infrastructure.database import (
SQLBaseModel, SqliteDecimal
)
from investing_algorithm_framework.infrastructure.models.model_extension \
import SQLAlchemyModelExtension
from investing_algorithm_framework.infrastructure.models\
Expand Down Expand Up @@ -56,16 +58,16 @@ class SQLTrade(Trade, SQLBaseModel, SQLAlchemyModelExtension):
trading_symbol = Column(String)
closed_at = Column(DateTime, default=None)
opened_at = Column(DateTime, default=None)
open_price = Column(Float, default=None)
amount = Column(Float, default=None)
available_amount = Column(Float, default=None)
filled_amount = Column(Float, default=None)
remaining = Column(Float, default=None)
net_gain = Column(Float, default=0)
cost = Column(Float, default=0)
last_reported_price = Column(Float, default=None)
open_price = Column(SqliteDecimal(), default=None)
amount = Column(SqliteDecimal(), default=None)
available_amount = Column(SqliteDecimal(), default=None)
filled_amount = Column(SqliteDecimal(), default=None)
remaining = Column(SqliteDecimal(), default=None)
net_gain = Column(SqliteDecimal(), default=0)
cost = Column(SqliteDecimal(), default=0)
last_reported_price = Column(SqliteDecimal(), default=None)
last_reported_price_datetime = Column(DateTime, default=None)
high_water_mark = Column(Float, default=None)
high_water_mark = Column(SqliteDecimal(), default=None)
high_water_mark_datetime = Column(DateTime, default=None)
updated_at = Column(DateTime, default=None)
status = Column(String, default=TradeStatus.CREATED.value)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from sqlalchemy import Column, Integer, String, Float, ForeignKey, Boolean, \
from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, \
DateTime
from sqlalchemy.orm import relationship

from investing_algorithm_framework.domain import TradeStopLoss
from investing_algorithm_framework.infrastructure.database import SQLBaseModel
from investing_algorithm_framework.infrastructure.database import (
SQLBaseModel, SqliteDecimal
)
from investing_algorithm_framework.infrastructure.models.model_extension \
import SQLAlchemyModelExtension

Expand Down Expand Up @@ -42,16 +44,16 @@ class SQLTradeStopLoss(TradeStopLoss, SQLBaseModel, SQLAlchemyModelExtension):
trade_id = Column(Integer, ForeignKey('trades.id'))
trade = relationship('SQLTrade', back_populates='stop_losses')
trailing = Column(Boolean)
percentage = Column(Float)
sell_percentage = Column(Float)
open_price = Column(Float)
high_water_mark = Column(Float)
percentage = Column(SqliteDecimal())
sell_percentage = Column(SqliteDecimal())
open_price = Column(SqliteDecimal())
high_water_mark = Column(SqliteDecimal())
high_water_mark_date = Column(String)
stop_loss_price = Column(Float)
stop_loss_price = Column(SqliteDecimal())
sell_prices = Column(String)
sell_dates = Column(String)
sell_amount = Column(Float)
sold_amount = Column(Float)
sell_amount = Column(SqliteDecimal())
sold_amount = Column(SqliteDecimal())
active = Column(Boolean)
triggered = Column(Boolean, default=False)
triggered_at = Column(DateTime, default=None)
Expand Down
Loading
Loading