Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pip install -r requirements.txt

## List all portfolios:
```python
(env): $ python script.py list-portfolios
(env): $ python cli.py list-portfolios
Output:
Your Portfolios:
- Strategies (ID: 961635)
Expand Down
218 changes: 190 additions & 28 deletions cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import os
import requests
import click
import datetime
from dateutil.relativedelta import relativedelta
import requests
import yfinance as yf
from dotenv import load_dotenv
load_dotenv()

Expand Down Expand Up @@ -54,7 +57,24 @@ def get_holdings(portfolio_id):
return response.json()["holdings"]
else:
raise Exception(f"Error fetching holdings: {response.status_code} {response.text}")


def get_stock_price(symbol, date):
"""Get the stock price for a given symbol and date."""
formatted_date = date.strftime("%Y-%m-%d")
print(formatted_date)
try:
stock = yf.Ticker(f"{symbol}.AX")
df = stock.history(start=formatted_date, end=formatted_date)
if not df.empty:
print(df)
return df["Close"].values[0]
else:
print(f"No price data found for {symbol} on {formatted_date}.")
return None
except Exception as e:
print(f"Exception while fetching price for {symbol} on {formatted_date}: {e}")
return None

def delete(portfolio_id):
"""Delete a portfolio with the given ID - uses V2 legacy API endpoint"""
global ACCESS_TOKEN
Expand All @@ -67,16 +87,51 @@ def delete(portfolio_id):
else:
raise Exception(f"Error deleting portfolio: {response.status_code} {response.text}")

# Define a new function to delete a portfolio with the given portfolio ID
def delete_portfolio_func(portfolio_id):
try:
delete(portfolio_id)
except Exception as e:
click.echo(f"Error deleting portfolio: {e}", err=True)

# Define a new function to create a portfolio
def create_portfolio_func(name):
global ACCESS_TOKEN
url = f"{API_BASE_URL}/{LEGACY_VERSION}/portfolios"
headers = {"Authorization": f"Bearer {ACCESS_TOKEN}"}

try:
response = requests.post(url, headers=headers, json={"name": name})
response.raise_for_status()
click.echo("Portfolio created successfully.")
except requests.exceptions.RequestException as e:
raise Exception(f"Error creating portfolio: {e}")

@click.group()
def cli():
"""ShareSight Portfolio Manager CLI"""
"""Sharesight CLI"""
try:
global ACCESS_TOKEN
ACCESS_TOKEN = request_access_token()
except Exception as e:
click.echo(f"Error: {e}", err=True)
raise click.Abort()


# Define the CLI command for deleting a portfolio
@cli.command()
@click.argument("portfolio_id", type=int)
def delete_portfolio(portfolio_id):
"""Delete a portfolio with the given portfolio ID."""
delete_portfolio_func(portfolio_id)

# Define the CLI command for creating a portfolio
@cli.command()
@click.argument("name")
def create_portfolio(name):
"""Create a new portfolio."""
create_portfolio_func(name)

@cli.command()
def list_portfolios():
"""List all portfolios."""
Expand All @@ -100,41 +155,148 @@ def list_holdings(portfolio_id):
except Exception as e:
click.echo(f"Error fetching holdings: {e}", err=True)

@cli.command()
@click.argument("portfolio_id", type=int)
def delete_portfolio(portfolio_id):
"""Delete a portfolio with a given ID."""
def get_periodic_dates(start_date, end_date, periodic_buy):
dates = []
current_date = start_date
while current_date <= end_date:
dates.append(current_date) # Append datetime.date object instead of string
if periodic_buy == "weekly":
current_date += datetime.timedelta(weeks=1)
elif periodic_buy == "monthly":
current_date += relativedelta(months=1)
elif periodic_buy == "quarterly":
current_date += relativedelta(months=3)
elif periodic_buy == "semi-annual":
current_date += relativedelta(months=6)
elif periodic_buy == "annual":
current_date += relativedelta(years=1)
return dates

def push_trade_to_sharesight(trade):
"""Push a trade to Sharesight."""
global ACCESS_TOKEN
url = f"{API_BASE_URL}{LEGACY_VERSION}/trades.json"
headers = {"Authorization": f"Bearer {ACCESS_TOKEN}"}

try:
delete(portfolio_id)
except Exception as e:
click.echo(f"Error deleting portfolio: {e}", err=True)
response = requests.post(url, headers=headers, json={"trade": trade})
response.raise_for_status()
print(response)
except requests.exceptions.RequestException as e:
raise Exception(f"Error pushing trade to Sharesight: {e}")

def push_trades(total_capital, start_date, end_date, sorted_rows, weights, portfolio_id, periodic_buy):

# Convert start_date and end_date to strings
start_date_str = start_date.strftime("%Y-%m-%d")
end_date_str = end_date.strftime("%Y-%m-%d")
num_days = (end_date - start_date).days
if periodic_buy == "weekly":
num_buy_orders = max(num_days // 7, 1)
elif periodic_buy == "monthly":
num_buy_orders = max(num_days // 30, 1)
elif periodic_buy == "quarterly":
num_buy_orders = max(num_days // 90, 1)
elif periodic_buy == "semi-annual":
num_buy_orders = max(num_days // 180, 1)
elif periodic_buy == "annual":
num_buy_orders = max(num_days // 365, 1)

print(num_buy_orders)

# Push trades to Sharesight for each buy order
for row, weight in zip(sorted_rows, weights):
# Calculate the capital for this row based on its weight
row_capital = total_capital * weight

# Calculate the amount of capital for each buy order
capital_per_buy_order = row_capital / num_buy_orders

for buy_date in get_periodic_dates(start_date, end_date, periodic_buy):
try:
stock_price = get_stock_price(row["ticker"], buy_date)
except Exception as e:
print(f"Error: {e}")
continue # Skip this trade and proceed with the next one

if stock_price is None:
print(f"Skipping trade for {row['ticker']} on {buy_date}: No price data found.")
continue # Skip this trade and proceed with the next one

# Continue with the rest of the trade process using the stock_price
# Calculate the number of units to buy, factoring in brokerage fees
units_to_buy = (capital_per_buy_order - 9.95) / stock_price

# Create a trade for this row with the calculated units to buy
trade = {
"unique_identifier": f"{row['ticker']}-{buy_date}",
"transaction_type": "BUY",
"transaction_date": buy_date.strftime("%Y-%m-%d"), # No need to format here
"portfolio_id": portfolio_id, # Use the user-input portfolio ID here
"symbol": row["ticker"],
"market": "ASX", # Replace with the actual market code
"quantity": units_to_buy,
"price": stock_price,
"exchange_rate": 1.0, # Replace with the actual exchange rate if applicable
}

# Call the function to push the trade to Sharesight
push_trade_to_sharesight(trade)

click.echo("Trades were pushed to Sharesight successfully.")

def get_user_input():
portfolio_id = click.prompt("Enter portfolio ID:", type=int)
total_capital = click.prompt("Enter total capital:", type=float, default=100000)
start_date = click.prompt("Enter start date (YYYY-MM-DD):", type=click.DateTime(formats=["%Y-%m-%d"]))
end_date = click.prompt("Enter end date (YYYY-MM-DD):", type=click.DateTime(formats=["%Y-%m-%d"]))
periodic_buy = click.prompt("Enter periodic buy (weekly/monthly/quarterly/semi-annual/annual):",
type=click.Choice(["weekly", "monthly", "quarterly", "semi-annual", "annual"]))
return total_capital, start_date, end_date, periodic_buy, portfolio_id

@cli.command()
@click.argument("file", type=click.Path(exists=True))
def read_csv(file):
"""Generate a list of securities from CSV"""
@click.pass_context
def build_portfolio_from_csv(ctx, file):
"""Generate a list of securities from CSV and designs a value weighted portfolio (experimental)"""
rows = tools.read.read_data_from_csv(file)
value_scores = [] # Store the value scores for each row

for row in rows:
# Get the ticker from each row
ticker = row["TICKER"]
# Get additional fields using the get_fields function
ticker = row["ticker"]
row["value"] = float(row["value"])
additional_fields = tools.api.get_fields(ticker)
# Add the additional fields to the row
row.update(additional_fields)
row["value_score"] = tools.value.calculate_value_score(row)
value_scores.append(row["value_score"])

# Calculate the percentage difference between the current price and the 'value' field
if "current_price" in row and "VALUE" in row:
current_price = row["current_price"]
value = float(row["VALUE"])
if value != 0:
percentage_diff = abs((current_price - value) / value) * 100
else:
percentage_diff = 0.0
row["percentage_difference"] = percentage_diff

# Print the updated row
print(row)
# Call the function to assign weights based on value scores
weights = tools.weights.assign_weights(value_scores)

# Sort the rows by value score (in descending order)
sorted_rows = sorted(rows, key=lambda x: x["value_score"], reverse=True)

# Print the sorted rows with formatted stability and weight
for row, weight in zip(sorted_rows, weights):
# Convert value to float

# Format stability as float with 2 decimal places
stability = row.get("stability")
if stability is not None:
stability = round(float(stability) * 100, 2)
row["stability"] = stability

# Format weight as float with 2 decimal places
row["weight"] = round(weight * 100, 2)

# readable output
print(row)

# Ask the user if they want to push trades to Sharesight
if click.confirm("Do you want to push the trades to Sharesight?", default=False):
total_capital, start_date, end_date, periodic_buy, portfolio_id = get_user_input()
push_trades(total_capital=total_capital, start_date=start_date, end_date=end_date,
sorted_rows=sorted_rows, weights=weights, portfolio_id=portfolio_id, periodic_buy=periodic_buy)

if __name__ == "__main__":
cli()
14 changes: 14 additions & 0 deletions data/base-data.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
ticker,PLVR,value
NHC,50,6.04
BHP,80,42.57
FMG,65,18.6
WHC,55,10.7
WBC,80,28.47
RIO,80,123.94
WES,80,48.95
WDS,75,29.08
STO,70,10.09
TLS,80,6.17
WOR,55,18.3
LYC,55,9.08
CSL,80,192.86
17 changes: 0 additions & 17 deletions data/data.csv

This file was deleted.

Binary file added data/data.numbers
Binary file not shown.
Loading