Skip to content

Commit

Permalink
Merge pull request #352 from ImNotOssy/develop-bbaepro
Browse files Browse the repository at this point in the history
Develop bbaepro. Hooray! 🎉
  • Loading branch information
matthew55 authored Sep 10, 2024
2 parents c1795fb + 3a627dc commit 9dc3774
Show file tree
Hide file tree
Showing 6 changed files with 331 additions and 2 deletions.
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ HEADLESS="true"
# at the same brokerage with a comma, then separate account credentials with a colon
# BROKER=BROKER_USERNAME:BROKER_PASSWORD,OTHER_BROKER_USERNAME:OTHER_BROKER_PASSWORD

# BBAE Pro
# BBAE=BBAE_USERNAME:BBAE_PASSWORD
BBAE=

# Chase
# CHASE=CHASE_USERNAME:CHASE_PASSWORD:CELL_PHONE_LAST_FOUR:DEBUG(Optional) TRUE/FALSE
CHASE=
Expand Down Expand Up @@ -67,4 +71,4 @@ VANGUARD=

# Webull
# WEBULL=WEBULL_USERNAME:WEBULL_PASSWORD:WEBULL_DID:WEBULL_TRADING_PIN
WEBULL=
WEBULL=
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ For help:
- `<dry>`: boolean, Whether to run in `dry` mode (in which no transactions are made. Useful for testing). Set to `True`, `False`, or just write `dry` for`True`. Defaults to `True`, so if you want to run a real transaction, you must set this explicitly.

Note: There are two special keywords you can use when specifying accounts: `all` and `day1`. `all` will use every account that you have set up. `day1` will use "day 1" brokers, which are:
- BBAE
- Chase
- Fennel
- Firstrade
Expand All @@ -189,6 +190,15 @@ All brokers: separate account credentials with a colon (":"). For example, `SCHW

Some brokerages require `Playwright` to run. On Windows, the `playwright install` command might not be recognized. If this is the case, run `python -m playwright install` instead.

#### BBAE
Made by [ImNotOssy](https://github.com/ImNotOssy) using the [BBAE_investing_API](https://github.com/ImNotOssy/BBAE_investing_API). Go give them a ⭐
- `BBAE_USERNAME`
- `BBAE_PASSWORD`

`.env` file format:
- `BBAE=BBAE_USERNAME:BBAE_PASSWORD`
- Note: `BBAE_USERNAME` can either be email or phone number.

#### Chase
Made by [MaxxRK](https://github.com/MaxxRK/) using the [chaseinvest-api](https://github.com/MaxxRK/chaseinvest-api). Go give them a ⭐
- `CHASE_USERNAME`
Expand Down
7 changes: 6 additions & 1 deletion autoRSA.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from dotenv import load_dotenv

# Custom API libraries
from bbaeAPI import *
from chaseAPI import *
from fennelAPI import *
from fidelityAPI import *
Expand Down Expand Up @@ -51,6 +52,7 @@

# Global variables
SUPPORTED_BROKERS = [
"bbae",
"chase",
"fennel",
"fidelity",
Expand All @@ -65,6 +67,7 @@
"webull",
]
DAY1_BROKERS = [
"bbae",
"chase",
"fennel",
"firstrade",
Expand All @@ -81,6 +84,8 @@

# Account nicknames
def nicknames(broker):
if broker == "bb":
return "bbae"
if broker in ["fid", "fido"]:
return "fidelity"
if broker == "ft":
Expand Down Expand Up @@ -122,7 +127,7 @@ def fun_run(orderObj: stockOrder, command, botObj=None, loop=None):
globals()[fun_name](DOCKER=DOCKER_MODE, loop=loop),
broker,
)
elif broker.lower() in ["fennel", "firstrade", "public"]:
elif broker.lower() in ["bbae", "fennel", "firstrade", "public"]:
# Requires bot object and loop
orderObj.set_logged_in(
globals()[fun_name](botObj=botObj, loop=loop), broker
Expand Down
270 changes: 270 additions & 0 deletions bbaeAPI.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import asyncio
import os
import traceback
from io import BytesIO
from dotenv import load_dotenv

from bbae_invest_api import BBAEAPI
from helperAPI import (
Brokerage,
printAndDiscord,
printHoldings,
getOTPCodeDiscord,
maskString,
stockOrder,
send_captcha_to_discord,
getUserInputDiscord,
)


def bbae_init(BBAE_EXTERNAL=None, botObj=None, loop=None):
load_dotenv()
bbae_obj = Brokerage("BBAE")
if not os.getenv("BBAE") and BBAE_EXTERNAL is None:
print("BBAE not found, skipping...")
return None
BBAE = (
os.environ["BBAE"].strip().split(",")
if BBAE_EXTERNAL is None
else BBAE_EXTERNAL.strip().split(",")
)
print("Logging in to BBAE...")
for index, account in enumerate(BBAE):
name = f"BBAE {index + 1}"
try:
user, password = account.split(":")[:2]
use_email = "@" in user
# Initialize the BBAE API object
bb = BBAEAPI(user, password, filename=f"BBAE_{index + 1}.pkl", creds_path="./creds/")
bb.make_initial_request()
# All the rest of the requests responsible for getting authenticated
login(bb, botObj, name, loop, use_email)
account_assets = bb.get_account_assets()
account_info = bb.get_account_info()
account_number = str(account_info["Data"]["accountNumber"])
# Set account values
masked_account_number = maskString(account_number)
bbae_obj.set_account_number(name, masked_account_number)
bbae_obj.set_account_totals(name, masked_account_number, float(account_assets["Data"]["totalAssets"]))
bbae_obj.set_logged_in_object(name, bb, "bb")
except Exception as e:
print(f"Error logging into BBAE: {e}")
print(traceback.format_exc())
continue
print("Logged into BBAE!")
return bbae_obj


def login(bb: BBAEAPI, botObj, name, loop, use_email):
try:
# API call to generate the login ticket
if use_email:
ticket_response = bb.generate_login_ticket_email()
else:
ticket_response = bb.generate_login_ticket_sms()
# Ensure "Data" key exists and proceed with verification if necessary
if ticket_response.get("Data") is None:
raise Exception("Invalid response from generating login ticket")
# Check if SMS or CAPTCHA verification are required
data = ticket_response["Data"]
if data.get("needSmsVerifyCode", False):
sms_and_captcha_response = handle_captcha_and_sms(bb, botObj, data, loop, name, use_email)
if not sms_and_captcha_response:
raise Exception("Error solving SMS or Captcha")
# Get the OTP code from the user
if botObj is not None and loop is not None:
otp_code = asyncio.run_coroutine_threadsafe(
getOTPCodeDiscord(botObj, name, timeout=300, loop=loop),
loop,
).result()
else:
otp_code = input("Enter security code: ")
if otp_code is None:
raise Exception("No SMS code received")
# Login with the OTP code
if use_email:
ticket_response = bb.generate_login_ticket_email(sms_code=otp_code)
else:
ticket_response = bb.generate_login_ticket_sms(sms_code=otp_code)
if ticket_response.get("Message") == "Incorrect verification code.":
raise Exception("Incorrect OTP code")
# Handle the login ticket
if ticket_response.get("Data") is not None and ticket_response.get("Data").get("ticket") is not None:
ticket = ticket_response["Data"]["ticket"]
else:
print(f"{name}: Raw response object: {ticket_response}")
raise Exception(f"Login failed. No ticket generated. Response: {ticket_response}")
login_response = bb.login_with_ticket(ticket)
if login_response.get("Outcome") != "Success":
raise Exception(f"Login failed. Response: {login_response}")
return True
except Exception as e:
print(f"Error in SMS login: {e}")
print(traceback.format_exc())
return False


def handle_captcha_and_sms(bb, botObj, data, loop, name, use_email):
try:
# If CAPTCHA is needed it will generate an SMS code as well
if data.get("needCaptchaCode", False):
print(f"{name}: CAPTCHA required. Requesting CAPTCHA image...")
sms_response = solve_captcha(bb, botObj, name, loop, use_email)
if not sms_response:
raise Exception("Failure solving CAPTCHA!")
else:
print(f"{name}: Requesting SMS code...")
sms_response = send_sms_code(bb, name, use_email)
if not sms_response:
raise Exception("Unable to retrieve sms code!")
return True
except Exception as e:
print(f"Error in CAPTCHA or SMS: {e}")
print(traceback.format_exc())
return False


def solve_captcha(bb: BBAEAPI, botObj, name, loop, use_email):
try:
captcha_image = bb.request_captcha()
if not captcha_image:
raise Exception("Unable to request CAPTCHA image, aborting...")
# Send the CAPTCHA image to Discord for manual input
file = BytesIO()
captcha_image.save(file, format="PNG")
file.seek(0)
# Retrieve input
if botObj is not None and loop is not None:
asyncio.run_coroutine_threadsafe(
send_captcha_to_discord(file),
loop,
).result()
captcha_input = asyncio.run_coroutine_threadsafe(
getUserInputDiscord(botObj, f"{name} requires CAPTCHA input", timeout=300, loop=loop),
loop,
).result()
else:
captcha_image.save("./captcha.png", format="PNG")
captcha_input = input("CAPTCHA image saved to ./captcha.png. Please open it and type in the code: ")
if captcha_input is None:
raise Exception("No CAPTCHA code found")
# Send the CAPTCHA to the appropriate API based on login type
if use_email:
sms_request_response = bb.request_email_code(captcha_input=captcha_input)
else:
sms_request_response = bb.request_sms_code(captcha_input=captcha_input)
if sms_request_response.get("Message") == "Incorrect verification code.":
raise Exception("Incorrect CAPTCHA code!")
return sms_request_response
except Exception as e:
print(f"{name}: Error solving CAPTCHA code: {e}")
print(traceback.format_exc())
return None


def send_sms_code(bb: BBAEAPI, name, use_email, captcha_input=None):
if use_email:
sms_code_response = bb.request_email_code(captcha_input=captcha_input)
else:
sms_code_response = bb.request_sms_code(captcha_input=captcha_input)
if sms_code_response.get("Message") == "Incorrect verification code.":
print(f"{name}: Incorrect CAPTCHA code, retrying...")
return False
return sms_code_response


def bbae_holdings(bbo: Brokerage, loop=None):
for key in bbo.get_account_numbers():
for account in bbo.get_account_numbers(key):
obj: BBAEAPI = bbo.get_logged_in_objects(key, "bb")
try:
positions = obj.get_account_holdings()
if positions.get("Data") is not None:
for holding in positions["Data"]:
qty = holding["CurrentAmount"]
if float(qty) == 0:
continue
sym = holding["displaySymbol"]
cp = holding["Last"]
bbo.set_holdings(key, account, sym, qty, cp)
except Exception as e:
printAndDiscord(f"Error getting BBAE holdings: {e}")
print(traceback.format_exc())
continue
printHoldings(bbo, loop, False)


def bbae_transaction(bbo: Brokerage, orderObj: stockOrder, loop=None):
print()
print("==============================")
print("BBAE")
print("==============================")
print()
for s in orderObj.get_stocks():
for key in bbo.get_account_numbers():
action = orderObj.get_action().lower()
printAndDiscord(
f"{key}: {action}ing {orderObj.get_amount()} of {s}",
loop,
)
for account in bbo.get_account_numbers(key):
obj: BBAEAPI = bbo.get_logged_in_objects(key, "bb")
try:
quantity = orderObj.get_amount()
is_dry_run = orderObj.get_dry()
# Buy
if action == "buy":
# Validate the buy transaction
validation_response = obj.validate_buy(symbol=s, amount=quantity, order_side=1, account_number=account)
if validation_response["Outcome"] != "Success":
printAndDiscord(f"{key} {account}: Validation failed for buying {quantity} of {s}: {validation_response["Message"]}", loop)
continue
# Proceed to execute the buy if not in dry run mode
if not is_dry_run:
buy_response = obj.execute_buy(
symbol=s,
amount=quantity,
account_number=account,
dry_run=is_dry_run
)
message = buy_response["Message"]
else:
message = "Dry Run Success"
# Sell
elif action == "sell":
# Check stock holdings before attempting to sell
holdings_response = obj.check_stock_holdings(symbol=s, account_number=account)
if holdings_response["Outcome"] != "Success":
printAndDiscord(f"{key} {account}: Error checking holdings: {holdings_response["Message"]}", loop)
continue
available_amount = float(holdings_response["Data"]["enableAmount"])
# If trying to sell more than available, skip to the next
if quantity > available_amount:
printAndDiscord(f"{key} {account}: Not enough shares to sell {quantity} of {s}. Available: {available_amount}", loop)
continue
# Validate the sell transaction
validation_response = obj.validate_sell(symbol=s, amount=quantity, account_number=account)
if validation_response["Outcome"] != "Success":
printAndDiscord(f"{key} {account}: Validation failed for selling {quantity} of {s}: {validation_response["Message"]}", loop)
continue
# Proceed to execute the sell if not in dry run mode
if not is_dry_run:
entrust_price = validation_response["Data"]["entrustPrice"]
sell_response = obj.execute_sell(
symbol=s,
amount=quantity,
account_number=account,
entrust_price=entrust_price,
dry_run=is_dry_run
)
message = sell_response["Message"]
else:
message = "Dry Run Success"
printAndDiscord(
f"{key}: {orderObj.get_action().capitalize()} {quantity} of {s} in {account}: {message}",
loop,
)
except Exception as e:
printAndDiscord(f"{key} {account}: Error placing order: {e}", loop)
print(traceback.format_exc())
continue
Loading

0 comments on commit 9dc3774

Please sign in to comment.