Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Public.com Support #150

Merged
merged 43 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
b6ba57b
init public
NelsonDane Jan 25, 2024
48ea786
public cli/discord 2fa support
NelsonDane Jan 27, 2024
4610220
Merge branch 'main' into develop-public
NelsonDane Jan 27, 2024
f2a806e
add public
NelsonDane Jan 27, 2024
b71bb60
style: format code with Black and isort
deepsource-autofix[bot] Jan 27, 2024
2178a4f
rename bot
NelsonDane Jan 27, 2024
2d41dc3
Merge pull request #151 from NelsonDane/deepsource-transform-3e617e7c
NelsonDane Jan 27, 2024
4cf724c
Merge branch 'main' into develop-public
NelsonDane Jan 30, 2024
3c25e03
update public
NelsonDane Jan 30, 2024
c67173b
fix package name
NelsonDane Jan 30, 2024
27d2c72
Merge branch 'main' into develop-public
NelsonDane Jan 31, 2024
fee8d19
style: format code with Black and isort
deepsource-autofix[bot] Jan 31, 2024
7f64841
Merge pull request #156 from NelsonDane/deepsource-transform-c1e1159a
NelsonDane Jan 31, 2024
d48b456
Merge branch 'main' into develop-public
NelsonDane Jan 31, 2024
e5807c3
Merge branch 'develop-public' of https://github.com/NelsonDane/auto-r…
NelsonDane Jan 31, 2024
f10f26d
public success message
NelsonDane Jan 31, 2024
0d7fcb3
Merge branch 'main' into develop-public
NelsonDane Feb 8, 2024
263e793
public phone fix
NelsonDane Feb 8, 2024
ad36423
Merge branch 'main' into develop-public
NelsonDane Feb 8, 2024
4e9f47c
creds folder
NelsonDane Feb 8, 2024
f30a9c5
add creds folder
NelsonDane Feb 8, 2024
042879d
Merge branch 'main' into develop-public
NelsonDane Feb 10, 2024
b0e1c5e
fix public login flow
NelsonDane Feb 10, 2024
a8b4c81
fix bot not printing
NelsonDane Feb 10, 2024
4fa3ff2
Merge branch 'main' into develop-public
NelsonDane Feb 12, 2024
4c70829
Merge branch 'main' into develop-public
NelsonDane Feb 15, 2024
9025cfb
Merge branch 'main' into develop-public
NelsonDane Feb 16, 2024
9808ba0
Merge branch 'main' into develop-public
NelsonDane Feb 21, 2024
fd7d5cf
Merge branch 'main' into develop-public
NelsonDane Feb 21, 2024
2a5e907
Merge branch 'main' into develop-public
NelsonDane Feb 21, 2024
7b2d84c
Merge branch 'main' into develop-public
NelsonDane Feb 22, 2024
ad95d24
fix 2fa not printing
NelsonDane Feb 22, 2024
6f3ad68
crypto holdings
NelsonDane Feb 22, 2024
938bbe2
Merge branch 'main' into develop-public
NelsonDane Feb 23, 2024
959af69
Merge branch 'main' into develop-public
NelsonDane Mar 11, 2024
f44cb14
Merge branch 'main' into develop-public
NelsonDane Mar 15, 2024
84980b2
style: format code with Black and isort
deepsource-autofix[bot] Mar 15, 2024
bf0370d
Merge pull request #192 from NelsonDane/deepsource-transform-d1a966d8
NelsonDane Mar 15, 2024
ea2926d
fix day1 order
NelsonDane Mar 15, 2024
95bbc9e
fix public env
Mar 16, 2024
5d7d715
update public broker
NelsonDane Mar 18, 2024
471b8b6
fix public multi account holdings
NelsonDane Mar 20, 2024
334bbef
Merge branch 'main' into develop-public
NelsonDane Mar 22, 2024
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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ FIDELITY=
# FIRSTRADE=FIRSTRADE_USERNAME:FIRSTRADE_PASSWORD:FIRSTRADE_PIN
FIRSTRADE=

# Public
# PUBLIC_BROKER=PUBLIC_USERNAME:PUBLIC_PASSWORD
PUBLIC_BROKER=

# Robinhood
# If 2fa is enabled:
# ROBINHOOD=ROBINHOOD_USERNAME:ROBINHOOD_PASSWORD:ROBINHOOD_TOTP
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ COPY ./entrypoint.sh .
COPY ./fidelityAPI.py .
COPY ./firstradeAPI.py .
COPY ./helperAPI.py .
COPY ./publicAPI.py .
COPY ./robinhoodAPI.py .
COPY ./schwabAPI.py .
COPY ./tastyAPI.py .
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,18 @@ Required `.env` variables:
`.env` file format:
- `FIRSTRADE=FIRSTRADE_USERNAME:FIRSTRADE_PASSWORD:FIRSTRADE_PIN`

### Public
Made by yours truly using using [public-invest-api](https://github.com/NelsonDane/public-invest-api). Consider giving me a ⭐

Required `.env` variables:
- `PUBLIC_USERNAME`
- `PUBLIC_PASSWORD`

`.env` file format:
- `PUBLIC_BROKER=PUBLIC_USERNAME:PUBLIC_PASSWORD`

Note: Because Windows already has a `PUBLIC` environment variable, you will need to use `PUBLIC_BROKER` instead.

### Robinhood
Made using [robin_stocks](https://github.com/jmfernandes/robin_stocks). Go give them a ⭐

Expand Down
26 changes: 21 additions & 5 deletions autoRSA.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from fidelityAPI import *
from firstradeAPI import *
from helperAPI import check_package_versions, stockOrder, updater
from publicAPI import *
from robinhoodAPI import *
from schwabAPI import *
from tastyAPI import *
Expand All @@ -35,13 +36,22 @@
SUPPORTED_BROKERS = [
"fidelity",
"firstrade",
"public",
"robinhood",
"schwab",
"tastytrade",
"tradier",
"webull",
]
DAY1_BROKERS = [
"firstrade",
"public",
"robinhood",
"schwab",
"tastytrade",
"tradier",
"webull",
]
DAY1_BROKERS = ["robinhood", "firstrade", "schwab", "tastytrade", "tradier", "webull"]
DISCORD_BOT = False
DOCKER_MODE = False
DANGER_MODE = False
Expand All @@ -64,7 +74,7 @@ def nicknames(broker):

# Runs the specified function for each broker in the list
# broker name + type of function
def fun_run(orderObj: stockOrder, command, loop=None):
def fun_run(orderObj: stockOrder, command, botObj=None, loop=None):
if command in ["_init", "_holdings", "_transaction"]:
for broker in orderObj.get_brokers():
if broker in orderObj.get_notbrokers():
Expand All @@ -79,6 +89,10 @@ def fun_run(orderObj: stockOrder, command, loop=None):
orderObj.set_logged_in(
globals()[fun_name](DOCKER=DOCKER_MODE), broker
)
elif broker.lower() == "public":
orderObj.set_logged_in(
globals()[fun_name](botObj=botObj, loop=loop), broker
)
else:
orderObj.set_logged_in(globals()[fun_name](), broker)
# Verify broker is logged in
Expand Down Expand Up @@ -264,17 +278,19 @@ async def rsa(ctx, *args):
event_loop = asyncio.get_event_loop()
try:
# Login to brokers
await bot.loop.run_in_executor(None, fun_run, discOrdObj, "_init")
await bot.loop.run_in_executor(
None, fun_run, discOrdObj, "_init", bot, event_loop
)
# Validate order object
discOrdObj.order_validate()
# Get holdings or complete transaction
if discOrdObj.get_holdings():
await bot.loop.run_in_executor(
None, fun_run, discOrdObj, "_holdings", event_loop
None, fun_run, discOrdObj, "_holdings", None, event_loop
)
else:
await bot.loop.run_in_executor(
None, fun_run, discOrdObj, "_transaction", event_loop
None, fun_run, discOrdObj, "_transaction", None, event_loop
)
except Exception as err:
print(traceback.format_exc())
Expand Down
4 changes: 4 additions & 0 deletions creds/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ services:
env_file:
- .env
tty: true
volumes:
- ./creds:/app/creds

watchtower:
# Auto update the auto-rsa container every hour
Expand Down
38 changes: 38 additions & 0 deletions helperAPI.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import pkg_resources
import requests
from discord.ext import commands
from dotenv import load_dotenv
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromiumService
Expand Down Expand Up @@ -555,6 +556,43 @@ async def processQueue():
task_queue.task_done()


async def getSMSCodeDiscord(
botObj: commands.Bot, brokerName, code_len=6, timeout=60, loop=None
):
printAndDiscord(f"{brokerName} requires SMS code", loop)
printAndDiscord(
f"Please enter SMS code or type cancel within {timeout} seconds", loop
)
# Get SMS code from Discord
sms_code = None
while sms_code is None:
try:
code = await botObj.wait_for(
"message",
# Ignore bot messages and messages not in the correct channel
check=lambda m: m.author != botObj.user
and m.channel.id == int(os.getenv("DISCORD_CHANNEL")),
timeout=timeout,
)
except asyncio.TimeoutError:
printAndDiscord(
f"Timed out waiting for SMS code input for {brokerName}", loop
)
return None
if code.content.lower() == "cancel":
printAndDiscord(f"Cancelling SMS code for {brokerName}", loop)
return None
try:
sms_code = int(code.content)
except ValueError:
printAndDiscord("SMS code must be numbers only", loop)
continue
if len(code.content) != code_len:
printAndDiscord("SMS code must be 6 digits", loop)
continue
return sms_code


def maskString(string):
# Mask string (12345678 -> xxxx5678)
string = str(string)
Expand Down
144 changes: 144 additions & 0 deletions publicAPI.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import asyncio
import os
import traceback

from dotenv import load_dotenv
from public_invest_api import Public

from helperAPI import (
Brokerage,
getSMSCodeDiscord,
maskString,
printAndDiscord,
printHoldings,
stockOrder,
)


def public_init(PUBLIC_EXTERNAL=None, botObj=None, loop=None):
# Initialize .env file
load_dotenv()
# Import Public account
public_obj = Brokerage("Public")
if not os.getenv("PUBLIC_BROKER") and PUBLIC_EXTERNAL is None:
print("Public not found, skipping...")
return None
PUBLIC = (
os.environ["PUBLIC_BROKER"].strip().split(",")
if PUBLIC_EXTERNAL is None
else PUBLIC_EXTERNAL.strip().split(",")
)
# Log in to Public account
print("Logging in to Public...")
for index, account in enumerate(PUBLIC):
name = f"Public {index + 1}"
try:
account = account.split(":")
pb = Public(filename=f"public{index + 1}.pkl", path="./creds/")
try:
if botObj is None and loop is None:
# Login from CLI
pb.login(
username=account[0],
password=account[1],
wait_for_2fa=True,
)
else:
# Login from Discord and check for 2fa required message
pb.login(
username=account[0],
password=account[1],
wait_for_2fa=False,
)
except Exception as e:
if "2FA" in str(e) and botObj is not None and loop is not None:
# Sometimes codes take a long time to arrive
timeout = 300 # 5 minutes
sms_code = asyncio.run_coroutine_threadsafe(
getSMSCodeDiscord(botObj, name, timeout=timeout, loop=loop),
loop,
).result()
if sms_code is None:
raise Exception("No SMS code found")
pb.login(
username=account[0],
password=account[1],
wait_for_2fa=False,
code=sms_code,
)
else:
raise e
# Public only has one account
public_obj.set_logged_in_object(name, pb)
an = pb.get_account_number()
public_obj.set_account_number(name, an)
print(f"{name}: Found account {maskString(an)}")
atype = pb.get_account_type()
public_obj.set_account_type(name, an, atype)
cash = pb.get_account_cash()
public_obj.set_account_totals(name, an, cash)
except Exception as e:
print(f"Error logging in to Public: {e}")
print(traceback.format_exc())
continue
print("Logged in to Public!")
return public_obj


def public_holdings(pbo: Brokerage, loop=None):
for key in pbo.get_account_numbers():
for account in pbo.get_account_numbers(key):
obj: Public = pbo.get_logged_in_objects(key)
try:
# Get account holdings
positions = obj.get_positions()
if positions != []:
for holding in positions:
# Get symbol, quantity, and total value
sym = holding["instrument"]["symbol"]
qty = float(holding["quantity"])
current_price = obj.get_symbol_price(sym)
if current_price is None:
current_price = "N/A"
pbo.set_holdings(key, account, sym, qty, current_price)
except Exception as e:
printAndDiscord(f"{key}: Error getting account holdings: {e}", loop)
traceback.format_exc()
continue
printHoldings(pbo, loop)


def public_transaction(pbo: Brokerage, orderObj: stockOrder, loop=None):
print()
print("==============================")
print("Public")
print("==============================")
print()
for s in orderObj.get_stocks():
for key in pbo.get_account_numbers():
printAndDiscord(
f"{key}: {orderObj.get_action()}ing {orderObj.get_amount()} of {s}",
loop,
)
for account in pbo.get_account_numbers(key):
obj: Public = pbo.get_logged_in_objects(key)
print_account = maskString(account)
try:
order = obj.place_order(
symbol=s,
quantity=orderObj.get_amount(),
side=orderObj.get_action(),
order_type="market",
time_in_force="day",
is_dry_run=orderObj.get_dry(),
)
if order["success"] is True:
order = "Success"
printAndDiscord(
f"{key}: {orderObj.get_action()} {orderObj.get_amount()} of {s} in {print_account}: {order}",
loop,
)
except Exception as e:
printAndDiscord(f"{print_account}: Error placing order: {e}", loop)
traceback.print_exc()
continue
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ asyncio==3.4.3
discord.py==2.3.2
firstrade==0.0.14
GitPython==3.1.42
-e git+https://github.com/NelsonDane/public-invest-api.git@abc84ca23cbf765f2ce0a88484c6db3b4b8019b4#egg=public-invest-api
pyotp==2.9.0
python-dotenv==1.0.1
requests==2.31.0
Expand Down