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

Develop bbaepro #352

Merged
merged 33 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b77ae69
BBAEpro
ImNotOssy Aug 30, 2024
3d69fa5
deepsource style fixes
ImNotOssy Aug 30, 2024
644fa6e
Update bbaeAPI.py
ImNotOssy Aug 31, 2024
acd0aeb
Fix endpoint order & discord image sending
matthew55 Aug 31, 2024
837f8e0
Merge remote-tracking branch 'ImNotOssy/develop-bbaepro' into fork/Im…
matthew55 Aug 31, 2024
15eb95f
BBAE minor refactor & remove while loop
matthew55 Aug 31, 2024
c870584
Fix brokers list in autorsa.py
matthew55 Aug 31, 2024
948167b
Add BBAE email login support
matthew55 Aug 31, 2024
8e44572
implemented deepsource suggestion
ImNotOssy Sep 1, 2024
21dcb7b
Merge branch 'main' into develop-bbaepro
NelsonDane Sep 1, 2024
8708763
sell support
ImNotOssy Sep 2, 2024
7fb6fc5
added pip
ImNotOssy Sep 3, 2024
760bafa
Merge branch 'main' into develop-bbaepro
NelsonDane Sep 3, 2024
369b248
fix alphabetical
NelsonDane Sep 3, 2024
9e5cec3
Update requirements.txt
ImNotOssy Sep 3, 2024
4ec5fa7
fix package name
ImNotOssy Sep 3, 2024
1f714bb
Add BBAE console support & remove logging
matthew55 Sep 4, 2024
f780234
Merge branch 'main' into develop-bbaepro
ImNotOssy Sep 5, 2024
153f120
Update bbaeAPI.py
ImNotOssy Sep 5, 2024
f6d9032
Fix BBAE email authentication
matthew55 Sep 5, 2024
43af0f3
Remove unnecessary logging
matthew55 Sep 5, 2024
ee9421d
Update requirements.txt
ImNotOssy Sep 5, 2024
d2e3bff
Update requirements.txt
ImNotOssy Sep 6, 2024
cdb7fb9
Merge branch 'main' into develop-bbaepro
matthew55 Sep 6, 2024
e30008c
small clean
NelsonDane Sep 7, 2024
fff7ed4
Update bbaeAPI.py
ImNotOssy Sep 10, 2024
7cd6d52
Update .env.example
ImNotOssy Sep 10, 2024
1d615d6
Update requirements.txt
ImNotOssy Sep 10, 2024
4c867cd
Merge branch 'main' into develop-bbaepro
NelsonDane Sep 10, 2024
9baf097
fix alphabetical
NelsonDane Sep 10, 2024
22435e7
code cleanup
NelsonDane Sep 10, 2024
81d28af
helper cleanup
NelsonDane Sep 10, 2024
3a627dc
Add BBAE to README
matthew55 Sep 10, 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
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=
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
39 changes: 39 additions & 0 deletions helperAPI.py
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,45 @@ async def getOTPCodeDiscord(
return code.content


async def getUserInputDiscord(botObj: commands.Bot, prompt, timeout=60, loop=None):
printAndDiscord(prompt, loop)
printAndDiscord(f"Please enter the input or type cancel within {timeout} seconds", loop)
try:
code = await botObj.wait_for(
"message",
check=lambda m: m.author != botObj.user and m.channel.id == int(DISCORD_CHANNEL),
timeout=timeout,
)
except asyncio.TimeoutError:
printAndDiscord("Timed out waiting for input", loop)
return None
if code.content.lower() == "cancel":
printAndDiscord("Input canceled by user", loop)
return None
return code.content


async def send_captcha_to_discord(file):
BASE_URL = f"https://discord.com/api/v10/channels/{DISCORD_CHANNEL}/messages"
HEADERS = {
"Authorization": f"Bot {DISCORD_TOKEN}",
}
files = {
'file': ('captcha.png', file, 'image/png')
}
success = False
while not success:
response = requests.post(BASE_URL, headers=HEADERS, files=files)
if response.status_code == 200:
success = True
elif response.status_code == 429:
rate_limit = response.json()["retry_after"] * 2
await asyncio.sleep(rate_limit)
else:
print(f"Error sending CAPTCHA image: {response.status_code}: {response.text}")
break


def maskString(string):
# Mask string (12345678 -> xxxx5678)
string = str(string)
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
asyncio==3.4.3
bbae-invest-api==0.1.3
chaseinvest-api==0.2.4
discord.py==2.4.0
fennel-invest-api==1.1.0
Expand Down