├── .env.example ├── .gitignore ├── Dockerfile ├── data_access ├── DAL │ ├── coins_DAL.py │ ├── orders_DAL.py │ └── portfolio_DAL.py └── models │ ├── base.py │ ├── coin.py │ ├── paper_order.py │ └── portfolio_item.py ├── docker-compose.yml ├── enums └── currencies.py ├── main.py ├── readme.md ├── requirements.txt ├── services ├── coingecko_service.py └── trading_service.py └── utils └── load_env.py /.env.example: -------------------------------------------------------------------------------- 1 | CG_API_KEY = "YOUR_CG_API_KEY" 2 | 3 | # for use outside docker 4 | DATABASE_URL= "postgresql://admin:admin@localhost:5432/data" 5 | 6 | # Application Settings 7 | TAKE_PROFIT = "20" 8 | STOP_LOSS = "10" 9 | ORDER_AMOUNT = "50" 10 | PRICE_CHANGE = "3" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Virtual environment directories 7 | env/ 8 | venv/ 9 | .venv/ 10 | 11 | # Cache files 12 | *.cache 13 | *.log 14 | *.out 15 | *.pid 16 | *.pyc 17 | 18 | # Jupyter Notebook checkpoints 19 | .ipynb_checkpoints/ 20 | 21 | # Testing 22 | htmlcov/ 23 | .tox/ 24 | .nox/ 25 | coverage.* 26 | *.cover 27 | .cache 28 | .pytest_cache/ 29 | *.pytest_cache/ 30 | 31 | # Distribution / Packaging 32 | .Python 33 | build/ 34 | dist/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | *.wheel 39 | 40 | # IDE and editor specific files 41 | .vscode/ 42 | .idea/ 43 | *.sublime-project 44 | *.sublime-workspace 45 | 46 | # OS generated files 47 | .DS_Store 48 | Thumbs.db 49 | 50 | # Docker and environment files 51 | *.env 52 | docker-compose.override.yml 53 | 54 | # Miscellaneous 55 | *.swp 56 | *.lock 57 | *~ 58 | Steps.md -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a base image 2 | FROM python:3.11-slim 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Copy the requirements file and install dependencies 8 | COPY requirements.txt . 9 | RUN pip install --no-cache-dir -r requirements.txt 10 | 11 | # Copy the application code 12 | COPY . . 13 | 14 | # Set environment variables (optional) 15 | ENV PYTHONDONTWRITEBYTECODE 1 16 | ENV PYTHONUNBUFFERED 1 17 | 18 | # Expose the port if your app has an HTTP server (optional) 19 | EXPOSE 8000 20 | 21 | # Command to run the application 22 | CMD ["python", "main.py"] 23 | -------------------------------------------------------------------------------- /data_access/DAL/coins_DAL.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from sqlalchemy.exc import NoResultFound 3 | from typing import List, Optional, Union 4 | from datetime import datetime 5 | from ..models.coin import Coin, CoinPrice 6 | 7 | 8 | class CoinsDAL: 9 | def __init__(self, session: Session): 10 | self.session = session 11 | 12 | def get_all_coins(self) -> List[Coin]: 13 | return self.session.query(Coin).all() 14 | 15 | def get_coin_by_symbol(self, symbol: str) -> Optional[Coin]: 16 | try: 17 | return self.session.query(Coin).filter(Coin.symbol == symbol).one() 18 | except NoResultFound: 19 | return None 20 | 21 | def update_coin_pnl( 22 | self, symbol: str, new_realized_pnl: Optional[float] 23 | ) -> Optional[Coin]: 24 | coin = self.get_coin_by_symbol(symbol) 25 | if coin: 26 | coin.realized_pnl = new_realized_pnl 27 | self.session.commit() 28 | return coin 29 | return None 30 | 31 | def add_price_to_coin( 32 | self, symbol: str, timestamp: datetime, value: float 33 | ) -> Optional[CoinPrice]: 34 | coin = self.get_coin_by_symbol(symbol) 35 | if coin: 36 | new_price = CoinPrice(timestamp=timestamp, value=value, symbol=coin.symbol) 37 | self.session.add(new_price) 38 | self.session.commit() 39 | return new_price 40 | return None 41 | 42 | def get_coin_prices_by_symbol(self, symbol: str) -> List[CoinPrice]: 43 | coin = self.get_coin_by_symbol(symbol) 44 | if coin: 45 | return ( 46 | self.session.query(CoinPrice) 47 | .filter(CoinPrice.symbol == coin.id) 48 | .order_by(CoinPrice.timestamp) 49 | .all() 50 | ) 51 | return [] 52 | 53 | def get_current_price_for_coin(self, symbol: str) -> Optional[CoinPrice]: 54 | coin: Union[Coin, CoinPrice] = self.get_coin_by_symbol(symbol) 55 | if coin and coin.prices: 56 | 57 | # Get the most recent price directly from the related prices 58 | return max(coin.prices, key=lambda price: price.timestamp) 59 | return None 60 | 61 | def add_coin( 62 | self, symbol: str, coin_id: str, realized_pnl: Optional[float] = None 63 | ) -> Optional[Coin]: 64 | existing_coin = self.get_coin_by_symbol(symbol) 65 | if existing_coin: 66 | return None 67 | 68 | new_coin = Coin( 69 | symbol=symbol, coin_id=coin_id, realized_pnl=realized_pnl or 0.0, prices=[] 70 | ) 71 | self.session.add(new_coin) 72 | self.session.commit() 73 | return new_coin 74 | -------------------------------------------------------------------------------- /data_access/DAL/orders_DAL.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from sqlalchemy.exc import NoResultFound 3 | from typing import List, Literal, Optional, Union 4 | from datetime import datetime 5 | from ..models.paper_order import PaperOrder 6 | 7 | 8 | class OrdersDAL: 9 | def __init__(self, session: Session): 10 | self.session = session 11 | 12 | def insert_order( 13 | self, 14 | timestamp: datetime, 15 | buy_price: float, 16 | quantity: float, 17 | symbol: str, 18 | direction: str, 19 | ) -> PaperOrder: 20 | new_order = PaperOrder( 21 | timestamp=timestamp, 22 | buy_price=buy_price, 23 | quantity=quantity, 24 | symbol=symbol, 25 | direction=direction, 26 | ) 27 | self.session.add(new_order) 28 | self.session.commit() 29 | return new_order 30 | 31 | def get_order_by_symbol(self, symbol: str) -> Optional[PaperOrder]: 32 | try: 33 | return ( 34 | self.session.query(PaperOrder) 35 | .filter(PaperOrder.symbol == symbol) 36 | .order_by(PaperOrder.timestamp.desc()) 37 | .first() 38 | ) 39 | except NoResultFound: 40 | return None 41 | 42 | def get_all_orders( 43 | self, direction: Optional[Literal["BUY", "SELL"]] = None 44 | ) -> List[PaperOrder]: 45 | query = self.session.query(PaperOrder) 46 | 47 | if direction: 48 | query = query.filter(PaperOrder.direction == direction) 49 | 50 | return query.all() 51 | -------------------------------------------------------------------------------- /data_access/DAL/portfolio_DAL.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from sqlalchemy.exc import NoResultFound 3 | from typing import List, Optional 4 | from datetime import datetime 5 | from ..models.portfolio_item import PortfolioItem, PnLEntry 6 | 7 | 8 | class PortfolioDAL: 9 | def __init__(self, session: Session): 10 | self.session = session 11 | 12 | def get_portfolio_item_by_symbol(self, symbol: str) -> Optional[PortfolioItem]: 13 | try: 14 | return ( 15 | self.session.query(PortfolioItem) 16 | .filter(PortfolioItem.symbol == symbol) 17 | .one() 18 | ) 19 | except NoResultFound: 20 | return None 21 | 22 | def insert_portfolio_item( 23 | self, symbol: str, cost_basis: float, total_quantity: float 24 | ) -> PortfolioItem: 25 | new_item = PortfolioItem( 26 | symbol=symbol, cost_basis=cost_basis, total_quantity=total_quantity 27 | ) 28 | self.session.add(new_item) 29 | self.session.commit() 30 | return new_item 31 | 32 | def update_portfolio_item_by_symbol( 33 | self, 34 | symbol: str, 35 | cost_basis: float, 36 | additional_quantity: float, 37 | ) -> Optional[PortfolioItem]: 38 | portfolio_item = self.get_portfolio_item_by_symbol(symbol) 39 | if portfolio_item is None: 40 | return None 41 | 42 | portfolio_item.cost_basis = cost_basis 43 | portfolio_item.total_quantity += additional_quantity 44 | self.session.commit() 45 | return portfolio_item 46 | 47 | def add_pnl_entry_by_symbol( 48 | self, symbol: str, date: datetime, value: float 49 | ) -> Optional[PnLEntry]: 50 | portfolio_item = self.get_portfolio_item_by_symbol(symbol) 51 | 52 | if portfolio_item is None: 53 | return None 54 | 55 | pnl_entry = PnLEntry( 56 | date=date, value=value, portfolio_item_id=portfolio_item.id 57 | ) 58 | self.session.add(pnl_entry) 59 | self.session.commit() 60 | return pnl_entry 61 | -------------------------------------------------------------------------------- /data_access/models/base.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import declarative_base 2 | 3 | Base = declarative_base() 4 | -------------------------------------------------------------------------------- /data_access/models/coin.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Float, ForeignKey, Integer, String, DateTime 2 | from sqlalchemy.orm import relationship, declarative_base 3 | from data_access.models.base import Base 4 | 5 | 6 | class Coin(Base): 7 | __tablename__ = "coins" 8 | 9 | id = Column(Integer, primary_key=True, autoincrement=True) 10 | coin_id = Column(String, unique=True, nullable=False) 11 | symbol = Column(String, unique=True, nullable=False) 12 | realized_pnl = Column(Float, nullable=True) 13 | 14 | prices = relationship( 15 | "CoinPrice", back_populates="coin", cascade="all, delete-orphan" 16 | ) 17 | 18 | 19 | class CoinPrice(Base): 20 | __tablename__ = "coins_prices" 21 | 22 | id = Column(Integer, primary_key=True, autoincrement=True) 23 | symbol = Column(String, ForeignKey("coins.symbol"), nullable=False) 24 | timestamp = Column(DateTime, nullable=False) 25 | value = Column(Float, nullable=False) 26 | 27 | coin = relationship("Coin", back_populates="prices") 28 | -------------------------------------------------------------------------------- /data_access/models/paper_order.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | from sqlalchemy import Column, Float, String, DateTime, Integer 3 | from sqlalchemy.ext.declarative import declarative_base 4 | from data_access.models.base import Base 5 | 6 | 7 | class PaperOrder(Base): 8 | __tablename__ = "paper_orders" 9 | 10 | id = Column(Integer, primary_key=True, autoincrement=True) 11 | timestamp = Column(DateTime, nullable=False) 12 | buy_price = Column(Float, nullable=False) 13 | quantity = Column(Float, nullable=False) 14 | symbol = Column(String, nullable=False) 15 | direction = Column(String, nullable=False) 16 | direction: Literal[ 17 | "BUY", "SELL" 18 | ] # Type hinting for Python-side checking, not affecting DB schema 19 | -------------------------------------------------------------------------------- /data_access/models/portfolio_item.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Float, String, DateTime, Integer, ForeignKey 2 | from sqlalchemy.orm import relationship 3 | from sqlalchemy.ext.declarative import declarative_base 4 | from data_access.models.base import Base 5 | 6 | 7 | class PnLEntry(Base): 8 | __tablename__ = "pnl_entries" 9 | 10 | id = Column(Integer, primary_key=True, autoincrement=True) 11 | date = Column(DateTime, nullable=False) 12 | value = Column(Float, nullable=False) 13 | 14 | portfolio_item_id = Column( 15 | Integer, ForeignKey("portfolio_items.id"), nullable=False 16 | ) 17 | 18 | 19 | class PortfolioItem(Base): 20 | __tablename__ = "portfolio_items" 21 | 22 | id = Column(Integer, primary_key=True, autoincrement=True) 23 | cost_basis = Column(Float, nullable=False) 24 | total_quantity = Column(Float, nullable=False) 25 | symbol = Column(String, nullable=False) 26 | 27 | # Relationship with PnLEntry (One-to-Many: One PortfolioItem can have many PnLEntries) 28 | pnl_entries = relationship( 29 | "PnLEntry", backref="portfolio_item", cascade="all, delete" 30 | ) 31 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | app: 4 | container_name: app 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | depends_on: 9 | - db 10 | environment: 11 | - DATABASE_URL=postgresql://admin:admin@db:5432/data 12 | volumes: 13 | - .:/app 14 | 15 | db: 16 | container_name: db 17 | image: postgres:latest 18 | environment: 19 | POSTGRES_USER: admin 20 | POSTGRES_PASSWORD: admin 21 | POSTGRES_DB: data 22 | ports: 23 | - "5432:5432" 24 | volumes: 25 | - db_data:/var/lib/postgresql/data 26 | restart: always 27 | 28 | volumes: 29 | db_data: 30 | -------------------------------------------------------------------------------- /enums/currencies.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Currency(Enum): 5 | BTC = "btc" 6 | ETH = "eth" 7 | LTC = "ltc" 8 | BCH = "bch" 9 | BNB = "bnb" 10 | EOS = "eos" 11 | XRP = "xrp" 12 | XLM = "xlm" 13 | LINK = "link" 14 | DOT = "dot" 15 | YFI = "yfi" 16 | USD = "usd" 17 | AED = "aed" 18 | ARS = "ars" 19 | AUD = "aud" 20 | BDT = "bdt" 21 | BHD = "bhd" 22 | BMD = "bmd" 23 | BRL = "brl" 24 | CAD = "cad" 25 | CHF = "chf" 26 | CLP = "clp" 27 | CNY = "cny" 28 | CZK = "czk" 29 | DKK = "dkk" 30 | EUR = "eur" 31 | GBP = "gbp" 32 | GEL = "gel" 33 | HKD = "hkd" 34 | HUF = "huf" 35 | IDR = "idr" 36 | ILS = "ils" 37 | INR = "inr" 38 | JPY = "jpy" 39 | KRW = "krw" 40 | KWD = "kwd" 41 | LKR = "lkr" 42 | MMK = "mmk" 43 | MXN = "mxn" 44 | MYR = "myr" 45 | NGN = "ngn" 46 | NOK = "nok" 47 | NZD = "nzd" 48 | PHP = "php" 49 | PKR = "pkr" 50 | PLN = "pln" 51 | RUB = "rub" 52 | SAR = "sar" 53 | SEK = "sek" 54 | SGD = "sgd" 55 | THB = "thb" 56 | TRY = "try" 57 | TWD = "twd" 58 | UAH = "uah" 59 | VEF = "vef" 60 | VND = "vnd" 61 | ZAR = "zar" 62 | XDR = "xdr" 63 | XAG = "xag" 64 | XAU = "xau" 65 | BITS = "bits" 66 | SATS = "sats" 67 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import List 3 | from sqlalchemy import create_engine 4 | from sqlalchemy.orm import sessionmaker 5 | from data_access.DAL.orders_DAL import OrdersDAL 6 | from data_access.DAL.portfolio_DAL import PortfolioDAL 7 | from data_access.DAL.coins_DAL import CoinsDAL 8 | from data_access.models.coin import Coin 9 | from services.coingecko_service import CoinGecko 10 | from services.trading_service import TradingService 11 | from utils.load_env import * 12 | from datetime import datetime 13 | import time 14 | from data_access.models.base import Base 15 | 16 | logging.disable(logging.CRITICAL) 17 | 18 | print("Waiting For Database to mount...") 19 | time.sleep(5) 20 | 21 | # Create engine and session using the database URL from environment 22 | engine = create_engine(db_url, echo=True) 23 | Base.metadata.create_all(engine) 24 | Session = sessionmaker(bind=engine) 25 | session = Session() 26 | 27 | # Initialize DALs and Services 28 | coins_dal = CoinsDAL(session) 29 | orders_dal = OrdersDAL(session) 30 | portfolio_dal = PortfolioDAL(session) 31 | cg = CoinGecko() 32 | 33 | 34 | def initialize_coin_data(): 35 | # Create engine and session using the database URL from environment 36 | 37 | if len(coins_dal.get_all_coins()) > 0: 38 | print("DB already initalized, skipping...") 39 | return 40 | 41 | cg = CoinGecko() 42 | 43 | all_coins = cg.get_coin_list() 44 | 45 | # Add coins and their initial prices to the list 46 | for coin in all_coins: 47 | coins_dal.add_coin(coin.symbol, coin.coin_id) 48 | coins_dal.add_price_to_coin( 49 | coin.symbol, coin.prices[0].timestamp, coin.prices[0].value 50 | ) 51 | print(f"Added {len(all_coins)} coins.") 52 | print(f"Added Prices to {len(all_coins)} coins.") 53 | 54 | 55 | def update_coin_prices() -> List[Coin]: 56 | db_coins = coins_dal.get_all_coins() 57 | db_coins_ids = [coin.coin_id for coin in db_coins] 58 | 59 | if len(db_coins) == 0: 60 | print("There are no coins in the database, cannot add prices") 61 | return 62 | 63 | coin_list = cg.get_coin_list() 64 | new_coins = 0 65 | for coin in coin_list: 66 | if coin.coin_id not in db_coins_ids: 67 | new_coins += 1 68 | coins_dal.add_coin(coin.symbol, coin.coin_id) 69 | 70 | coins_dal.add_price_to_coin(coin.symbol, datetime.now(), coin.prices[0].value) 71 | print(f"Price updated for {len(db_coins)} coins") 72 | print( 73 | f"Inserted {new_coins} coins to the coins table likely due movements in the top 250." 74 | ) 75 | return coin_list 76 | 77 | 78 | # Function to handle buy logic 79 | def handle_buy(coin, current_price): 80 | if coin.price_change < price_change: 81 | return 82 | 83 | # Buy and add order to the table 84 | order = TradingService.buy(coin.symbol, current_price, qty) 85 | 86 | existing_portfolio = portfolio_dal.get_portfolio_item_by_symbol(order.symbol) 87 | 88 | if existing_portfolio is None: 89 | portfolio_dal.insert_portfolio_item( 90 | order.symbol, order.buy_price, order.quantity 91 | ) 92 | print( 93 | f"Bought {order.symbol} and inserted new portfolio item for {order.symbol}" 94 | ) 95 | else: 96 | cost_basis = TradingService.calculate_cost_basis( 97 | existing_portfolio.cost_basis, 98 | existing_portfolio.total_quantity, 99 | order.quantity, 100 | order.buy_price, 101 | ) 102 | portfolio_dal.update_portfolio_item_by_symbol( 103 | order.symbol, cost_basis, order.quantity 104 | ) 105 | print( 106 | f"Bought {order.symbol}. We already hold {order.symbol}, updating existing portfolio with new order data." 107 | ) 108 | orders_dal.insert_order( 109 | order.timestamp, order.buy_price, order.quantity, order.symbol, order.direction 110 | ) 111 | 112 | 113 | # Function to handle sell logic 114 | def handle_sell(coin, current_price): 115 | buy_orders = orders_dal.get_all_orders("BUY") 116 | 117 | # Filter buy orders for the current symbol 118 | filtered_buy_orders = [order for order in buy_orders if order.symbol == coin.symbol] 119 | 120 | if not filtered_buy_orders: 121 | return 122 | 123 | for order in filtered_buy_orders: 124 | stop_loss_price = order.buy_price * (1 - sl / 100) 125 | take_profit_price = order.buy_price * (1 + tp / 100) 126 | current_pnl = (current_price - order.buy_price) / order.buy_price * 100 127 | 128 | if current_price <= stop_loss_price: 129 | sell_order = TradingService.sell( 130 | order.symbol, current_price, order.quantity 131 | ) 132 | print( 133 | f"Stop Loss Triggered: Sold {order.quantity} of {order.symbol} at ${current_price}" 134 | ) 135 | 136 | elif current_price >= take_profit_price: 137 | sell_order = TradingService.sell( 138 | order.symbol, current_price, order.quantity 139 | ) 140 | print( 141 | f"Take Profit Triggered: Sold {order.quantity} of {order.symbol} at ${current_price}" 142 | ) 143 | else: 144 | continue 145 | orders_dal.insert_order( 146 | sell_order.timestamp, 147 | sell_order.buy_price, 148 | sell_order.quantity, 149 | sell_order.symbol, 150 | sell_order.direction, 151 | ) 152 | coins_dal.update_coin_pnl(order.symbol, current_pnl) 153 | 154 | 155 | # Main execution logic 156 | def main(): 157 | # Populate database with initial data 158 | initialize_coin_data() 159 | while True: 160 | api_coins = update_coin_prices() 161 | for coin in api_coins: 162 | current_price = coin.prices[0].value 163 | handle_buy(coin, current_price) 164 | handle_sell(coin, current_price) 165 | portfolio_dal.add_pnl_entry_by_symbol( 166 | coin.symbol, datetime.now(), coin.prices[0].value 167 | ) 168 | print("Engine cycle complete, sleeping for 1 hour.") 169 | time.sleep(3600) 170 | 171 | 172 | if __name__ == "__main__": 173 | main() 174 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Paper Trading Crypto Trading Bot in Python 2 | 3 | Simple paper trading crypto trading bot written in Python. The bot uses postgreSQL to store trading data and is fully dockerised. 4 | 5 | The bot trades based on CoinGecko's market data - specifically price change. This is hardcoded to 1hour price change percentage but can easily be adjusted to fit other requirements. 6 | 7 | ## To get started 8 | 9 | You will need Docker to run this tool. 10 | To get started, simply `run docker compose -t "paper_bot" up -d --build` 11 | 12 | You can see log outputs inside the `app` container. 13 | 14 | ## To Develop 15 | 16 | To add features or change the bot's logic you will need to install the python requirements locally with `pip install -r requirements.txt`. 17 | the bot is configured to run locally as well, so long as your db container is running in docker. 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2024.8.30 2 | charset-normalizer==3.4.0 3 | greenlet==3.1.1 4 | idna==3.10 5 | psycopg2-binary==2.9.10 6 | python-dotenv==1.0.1 7 | requests==2.32.3 8 | SQLAlchemy==2.0.36 9 | typing_extensions==4.12.2 10 | urllib3==2.2.3 -------------------------------------------------------------------------------- /services/coingecko_service.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from data_access.models.coin import Coin, CoinPrice 3 | from enums.currencies import Currency 4 | from utils.load_env import * 5 | from typing import List 6 | from datetime import datetime 7 | 8 | 9 | class CoinGecko: 10 | def __init__(self): 11 | self.root = "https://api.coingecko.com/api/v3" 12 | self.headers = { 13 | "accept": "application/json", 14 | "x-cg-demo-api-key": f"{cg_api_key}", 15 | } 16 | 17 | def get_price_by_coin_id(self, coin_id: str): 18 | request_url = self.root + f"/simple/price?ids={coin_id}&vs_currencies=usd" 19 | response = requests.get(request_url, self.headers).json() 20 | print(response) 21 | return response[coin_id]["usd"] 22 | 23 | def get_vs_currencies(self): 24 | request_url = self.root + "/simple/supported_vs_currencies" 25 | return requests.get(request_url, self.headers).json() 26 | 27 | def get_coin_list(self) -> List[Coin]: 28 | request_url = ( 29 | self.root 30 | + f"/coins/markets?order=market_cap_desc&per_page=250&vs_currency={Currency.USD}&price_change_percentage=1h" 31 | ) 32 | response = requests.get(request_url, headers=self.headers).json() 33 | 34 | coins = [] 35 | for coin_data in response: 36 | coin = Coin( 37 | coin_id=coin_data["id"], 38 | symbol=coin_data["symbol"], 39 | realized_pnl=None, 40 | ) 41 | 42 | price = CoinPrice( 43 | symbol=coin_data["symbol"], 44 | timestamp=datetime.now(), 45 | value=coin_data["current_price"], 46 | ) 47 | 48 | coin.prices = [price] 49 | coin.price_change = coin_data["price_change_percentage_1h_in_currency"] 50 | coins.append(coin) 51 | 52 | return coins 53 | -------------------------------------------------------------------------------- /services/trading_service.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from data_access.models.paper_order import PaperOrder 3 | 4 | 5 | class TradingService: 6 | def __init__(self): 7 | pass 8 | 9 | @staticmethod 10 | def buy(symbol: str, current_price: float, quantity: float) -> PaperOrder: 11 | return PaperOrder( 12 | timestamp=datetime.now(), 13 | buy_price=current_price, 14 | quantity=quantity, 15 | symbol=symbol, 16 | direction="BUY", 17 | ) 18 | 19 | @staticmethod 20 | def sell(symbol: str, current_price: float, quantity: float) -> PaperOrder: 21 | return PaperOrder( 22 | timestamp=datetime.now(), 23 | buy_price=current_price, 24 | quantity=quantity, 25 | symbol=symbol, 26 | direction="SELL", 27 | ) 28 | 29 | @staticmethod 30 | def calculate_cost_basis( 31 | current_cost_basis: float, 32 | total_qty: float, 33 | new_order_qty: float, 34 | new_order_price: float, 35 | ) -> float: 36 | new_total_quantity = total_qty + new_order_qty 37 | 38 | if new_total_quantity == 0: 39 | return 0 # If all quantities are sold, cost basis resets 40 | 41 | return ( 42 | (current_cost_basis * total_qty) + (new_order_price * new_order_qty) 43 | ) / new_total_quantity 44 | -------------------------------------------------------------------------------- /utils/load_env.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | load_dotenv() 5 | cg_api_key = os.getenv("CG_API_KEY") 6 | db_url = os.getenv("DATABASE_URL") 7 | 8 | tp = float(os.getenv("TAKE_PROFIT")) 9 | sl = float(os.getenv("STOP_LOSS")) 10 | qty = float(os.getenv("ORDER_AMOUNT")) 11 | price_change = float(os.getenv("PRICE_CHANGE")) 12 | --------------------------------------------------------------------------------