Skip to content

Commit

Permalink
update version
Browse files Browse the repository at this point in the history
  • Loading branch information
TexasCoding committed Jun 22, 2024
1 parent 49dce20 commit fea64d1
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 170 deletions.
32 changes: 16 additions & 16 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "py-alpaca-daily-losers"
version = "2.2.2"
version = "2.2.3"
description = "Daily Losers strategy, uses py-alpaca-api for Alpaca Markets integration."
authors = ["TexasCoding <jeff10278@me.com>"]
license = "MIT"
Expand Down
34 changes: 25 additions & 9 deletions src/alpaca_daily_losers/close_positions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import logging
from typing import List, Any
from typing import List

import pandas as pd
from py_alpaca_api import Stock, Trading
from alpaca_daily_losers.global_functions import get_ticker_data, send_message, send_position_messages

from alpaca_daily_losers.global_functions import (
get_ticker_data,
send_message,
send_position_messages,
)


class ClosePositions:
def __init__(self, trading_client: Trading, stock_client: Stock, py_logger: logging.Logger):
Expand All @@ -22,8 +29,8 @@ def sell_positions_from_criteria(
cash position, and then uses the `get_stocks_to_sell()` method to
determine which positions should be sold. It then calls
the `_sell_positions()` method to execute the sell orders and
sends messages to notify of the sold positions. If no positions meet the
sell criteria, a message is sent indicating that no sell opportunities were found.
sends messages to notify of the sold positions. If no positions meet the
sell criteria, a message is sent indicating that no sell opportunities were found.
Raises:
Exception: If an error occurs while selling the positions.
"""
Expand All @@ -42,7 +49,9 @@ def sell_positions_from_criteria(
except Exception as e:
self.py_logger.error(f"Error selling positions from criteria. Error: {e}")

def _sell_positions(self, stocks_to_sell: List[str], current_positions: pd.DataFrame) -> List[dict]:
def _sell_positions(
self, stocks_to_sell: List[str], current_positions: pd.DataFrame
) -> List[dict]:
"""
Sell positions for the given stocks.
Expand Down Expand Up @@ -101,12 +110,19 @@ def get_stocks_to_sell(
stocks_to_sell = sell_filtered_df["symbol"].tolist()

# Add stocks based on take profit and stop loss criteria
stocks_to_sell = self._add_stocks_from_criteria(non_cash_positions, stocks_to_sell, stop_loss_percentage, take_profit_percentage)
stocks_to_sell = self._add_stocks_from_criteria(
non_cash_positions, stocks_to_sell, stop_loss_percentage, take_profit_percentage
)

return stocks_to_sell

def _add_stocks_from_criteria(self, non_cash_positions: pd.DataFrame, stocks_to_sell: List[str],
stop_loss_percentage: float, take_profit_percentage: float) -> List[str]:
def _add_stocks_from_criteria(
self,
non_cash_positions: pd.DataFrame,
stocks_to_sell: List[str],
stop_loss_percentage: float,
take_profit_percentage: float,
) -> List[str]:
"""
Adds stocks to the sell list based on stop loss and take profit criteria.
Expand All @@ -131,4 +147,4 @@ def _add_stocks_from_criteria(self, non_cash_positions: pd.DataFrame, stocks_to_
if stock not in stocks_to_sell:
stocks_to_sell.append(stock)

return stocks_to_sell
return stocks_to_sell
58 changes: 43 additions & 15 deletions src/alpaca_daily_losers/daily_losers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import logging
import os
import re
from typing import List

import pandas as pd
from dotenv import load_dotenv
from py_alpaca_api import PyAlpacaAPI

from alpaca_daily_losers.close_positions import ClosePositions
from alpaca_daily_losers.global_functions import (
get_ticker_data,
Expand All @@ -16,9 +16,8 @@
from alpaca_daily_losers.openai import OpenAIAPI
from alpaca_daily_losers.statistics import Statistics


# Constants
WATCHLIST_NAME = 'DailyLosers'
WATCHLIST_NAME = "DailyLosers"
DEFAULT_BUY_LIMIT = 4
DEFAULT_ARTICLE_LIMIT = 4
DEFAULT_STOP_LOSS_PERCENTAGE = 10.0
Expand All @@ -44,13 +43,20 @@ class DailyLosers:
def __init__(self):
self.alpaca = PyAlpacaAPI(api_key=API_KEY, api_secret=API_SECRET, api_paper=API_PAPER)
self.liquidate = Liquidate(trading_client=self.alpaca.trading, py_logger=logger)
self.close = ClosePositions(trading_client=self.alpaca.trading, stock_client=self.alpaca.stock, py_logger=logger)
self.close = ClosePositions(
trading_client=self.alpaca.trading, stock_client=self.alpaca.stock, py_logger=logger
)
self.statistics = Statistics(account=self.alpaca.trading.account, py_logger=logger)
self.openai = OpenAIAPI()

def run(self, buy_limit=DEFAULT_BUY_LIMIT, article_limit=DEFAULT_ARTICLE_LIMIT,
stop_loss_percentage=DEFAULT_STOP_LOSS_PERCENTAGE, take_profit_percentage=DEFAULT_TAKE_PROFIT_PERCENTAGE,
future_days=DEFAULT_FUTURE_DAYS):
def run(
self,
buy_limit=DEFAULT_BUY_LIMIT,
article_limit=DEFAULT_ARTICLE_LIMIT,
stop_loss_percentage=DEFAULT_STOP_LOSS_PERCENTAGE,
take_profit_percentage=DEFAULT_TAKE_PROFIT_PERCENTAGE,
future_days=DEFAULT_FUTURE_DAYS,
):
"""
Executes the main logic of the program, orchestrating the various components.
"""
Expand All @@ -70,7 +76,12 @@ def run(self, buy_limit=DEFAULT_BUY_LIMIT, article_limit=DEFAULT_ARTICLE_LIMIT,
except Exception as e:
logger.error(f"Error entering new positions: {e}")

def check_for_buy_opportunities(self, buy_limit=DEFAULT_BUY_LIMIT, article_limit=DEFAULT_ARTICLE_LIMIT, future_days=DEFAULT_FUTURE_DAYS):
def check_for_buy_opportunities(
self,
buy_limit=DEFAULT_BUY_LIMIT,
article_limit=DEFAULT_ARTICLE_LIMIT,
future_days=DEFAULT_FUTURE_DAYS,
):
"""
Checks for buy opportunities based on daily losers and news sentiment.
"""
Expand All @@ -80,7 +91,7 @@ def check_for_buy_opportunities(self, buy_limit=DEFAULT_BUY_LIMIT, article_limit
print("No buy opportunities found.")
logger.info("No buy opportunities found.")
return

tickers = self.filter_tickers_with_news(losers, article_limit, buy_limit)
if tickers:
logger.info(f"Found {len(tickers)} buy opportunities.")
Expand Down Expand Up @@ -130,7 +141,12 @@ def update_or_create_watchlist(self, name: str, symbols: List[str]):
except Exception as e:
logger.error(f"Could not create or update the watchlist {name}: {e}")

def filter_tickers_with_news(self, tickers: List[str], article_limit=DEFAULT_ARTICLE_LIMIT, filter_ticker_limit=DEFAULT_BUY_LIMIT):
def filter_tickers_with_news(
self,
tickers: List[str],
article_limit=DEFAULT_ARTICLE_LIMIT,
filter_ticker_limit=DEFAULT_BUY_LIMIT,
):
"""
Filters tickers based on news sentiment.
"""
Expand All @@ -140,10 +156,20 @@ def filter_tickers_with_news(self, tickers: List[str], article_limit=DEFAULT_ART
if len(filtered_tickers) >= filter_ticker_limit:
break

articles = self.alpaca.trading.news.get_news(symbol=ticker, limit=article_limit, content_length=4000)
articles = self.alpaca.trading.news.get_news(
symbol=ticker, limit=article_limit, content_length=4000
)
if len(articles) >= article_limit:
bullish_count = sum(1 for article in articles if self.openai.get_sentiment_analysis(
title=article["title"], symbol=article["symbol"], article=article["content"]) == "BULLISH")
bullish_count = sum(
1
for article in articles
if self.openai.get_sentiment_analysis(
title=article["title"],
symbol=article["symbol"],
article=article["content"],
)
== "BULLISH"
)

if bullish_count > len(articles) // 2:
filtered_tickers.append(ticker)
Expand All @@ -160,7 +186,9 @@ def get_daily_losers(self, future_days=DEFAULT_FUTURE_DAYS):
"""
try:
losers = self.alpaca.stock.predictor.get_losers_to_gainers(future_periods=future_days)
losers_data = get_ticker_data(tickers=losers, stock_client=self.alpaca.stock, py_logger=logger)
losers_data = get_ticker_data(
tickers=losers, stock_client=self.alpaca.stock, py_logger=logger
)
return self.buy_criteria(losers_data)
except Exception as e:
logger.error(f"Error fetching daily losers: {e}")
Expand All @@ -183,4 +211,4 @@ def buy_criteria(self, data: pd.DataFrame):

logger.info(f"Found {len(filtered_symbols)} tickers that meet the buy criteria.")
self.update_or_create_watchlist(name=WATCHLIST_NAME, symbols=filtered_symbols)
return filtered_symbols
return filtered_symbols
9 changes: 7 additions & 2 deletions src/alpaca_daily_losers/global_functions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import os
from datetime import datetime, timedelta

import pandas as pd
from dotenv import load_dotenv
from py_alpaca_api import Stock
from pytz import timezone
from ta.momentum import RSIIndicator
from ta.volatility import BollingerBands
from py_alpaca_api import Stock

from alpaca_daily_losers.slack import Slack

load_dotenv()
Expand All @@ -21,6 +23,7 @@
production = os.getenv("PRODUCTION")
slack_username = os.getenv("SLACK_USERNAME")


def get_ticker_data(tickers, stock_client: Stock, py_logger) -> pd.DataFrame:
"""
Retrieve historical data for given tickers and compute technical indicators.
Expand Down Expand Up @@ -62,6 +65,7 @@ def get_ticker_data(tickers, stock_client: Stock, py_logger) -> pd.DataFrame:

return df_tech


def send_position_messages(positions: list, pos_type: str):
"""
Sends position messages based on the type of position.
Expand Down Expand Up @@ -96,6 +100,7 @@ def send_position_messages(positions: list, pos_type: str):

return send_message(position_message)


def send_message(message: str):
"""
Send a message to Slack.
Expand All @@ -107,4 +112,4 @@ def send_message(message: str):
if production == "False":
print(f"Message: {message}")
else:
slack.send_message(channel="#app-development", message=message, username=slack_username)
slack.send_message(channel="#app-development", message=message, username=slack_username)
19 changes: 14 additions & 5 deletions src/alpaca_daily_losers/liquidate.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import logging

import pandas as pd
from py_alpaca_api.trading import Trading

from alpaca_daily_losers.global_functions import send_message, send_position_messages


class Liquidate:
FIXED_FEE = 5.00

Expand Down Expand Up @@ -52,7 +55,7 @@ def liquidate_positions(self) -> None:
None
"""
current_positions = self.trade.positions.get_all()

if current_positions[current_positions["symbol"] != "Cash"].empty:
self._send_liquidation_message("No positions available to liquidate for capital")
return
Expand All @@ -65,10 +68,14 @@ def liquidate_positions(self) -> None:
top_performers = self.get_top_performers(current_positions)
top_performers_market_value = top_performers["market_value"].sum()
cash_needed = self.calculate_cash_needed(total_holdings, cash_row)
sold_positions = self._sell_top_performers(top_performers, top_performers_market_value, cash_needed)
sold_positions = self._sell_top_performers(
top_performers, top_performers_market_value, cash_needed
)
send_position_messages(sold_positions, "liquidate")

def _sell_top_performers(self, top_performers: pd.DataFrame, top_performers_market_value: float, cash_needed: float) -> list:
def _sell_top_performers(
self, top_performers: pd.DataFrame, top_performers_market_value: float, cash_needed: float
) -> list:
"""
Sells positions of top performers to liquidate the required cash.
Expand All @@ -88,7 +95,9 @@ def _sell_top_performers(self, top_performers: pd.DataFrame, top_performers_mark

try:
self.trade.orders.market(symbol=row["symbol"], notional=amount_to_sell, side="sell")
sold_positions.append({"symbol": row["symbol"], "notional": round(amount_to_sell, 2)})
sold_positions.append(
{"symbol": row["symbol"], "notional": round(amount_to_sell, 2)}
)
except Exception as e:
self.py_logger.warning(f"Error liquidating position {row['symbol']}. Error: {e}")
self._send_liquidation_message(f"Error selling {row['symbol']}: {e}")
Expand All @@ -103,4 +112,4 @@ def _send_liquidation_message(message: str):
Parameters:
message (str): The message to be sent.
"""
send_message(message)
send_message(message)
Loading

0 comments on commit fea64d1

Please sign in to comment.