Skip to content

Commit

Permalink
Merge pull request #353 from ImNotOssy/develop-dspac
Browse files Browse the repository at this point in the history
dSPAC Support
  • Loading branch information
MaxxRK authored Sep 22, 2024
2 parents 56ba5a1 + 582f992 commit e95ab01
Showing 5 changed files with 309 additions and 1 deletion.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -23,6 +23,10 @@ BBAE=
# CHASE=CHASE_USERNAME:CHASE_PASSWORD:CELL_PHONE_LAST_FOUR:DEBUG(Optional) TRUE/FALSE
CHASE=

# dSPAC
# DSPAC=DSPAC_USERNAME:DSPAC_PASSWORD
DSPAC=

# Fennel
# FENNEL=FENNEL_EMAIL
FENNEL=
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -165,6 +165,7 @@ For help:
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
- DSPAC
- Fennel
- Firstrade
- Public
@@ -211,6 +212,16 @@ Optional .env variables:
`.env` file format:
- `CHASE=CHASE_USERNAME:CHASE_PASSWORD:CELL_PHONE_LAST_FOUR:DEBUG`

#### DSPAC
Made by [ImNotOssy](https://github.com/ImNotOssy) using the [dSPAC_investing_api](https://github.com/ImNotOssy/dSPAC_investing_API). Go give them a ⭐
- `DSPAC_USERNAME`
- `DSPAC_PASSWORD`

`.env` file format:
- `DSPAC=DSPAC_USERNAME:DSPAC_PASSWORD`

Note: `DSPAC_USERNAME` can either be email or phone number.

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

8 changes: 7 additions & 1 deletion autoRSA.py
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@
# Custom API libraries
from bbaeAPI import *
from chaseAPI import *
from dspacAPI import *
from fennelAPI import *
from fidelityAPI import *
from firstradeAPI import *
@@ -54,6 +55,7 @@
SUPPORTED_BROKERS = [
"bbae",
"chase",
"dspac",
"fennel",
"fidelity",
"firstrade",
@@ -69,6 +71,7 @@
DAY1_BROKERS = [
"bbae",
"chase",
"dspac",
"fennel",
"firstrade",
"public",
@@ -86,6 +89,8 @@
def nicknames(broker):
if broker == "bb":
return "bbae"
if broker == "ds":
return "dspac"
if broker in ["fid", "fido"]:
return "fidelity"
if broker == "ft":
@@ -127,7 +132,8 @@ def fun_run(orderObj: stockOrder, command, botObj=None, loop=None):
globals()[fun_name](DOCKER=DOCKER_MODE, loop=loop),
broker,
)
elif broker.lower() in ["bbae", "fennel", "firstrade", "public"]:

elif broker.lower() in ["bbae", "dspac", "fennel", "firstrade", "public"]:
# Requires bot object and loop
orderObj.set_logged_in(
globals()[fun_name](botObj=botObj, loop=loop), broker
286 changes: 286 additions & 0 deletions dspacAPI.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
import asyncio
import os
import traceback
from io import BytesIO

from dspac_invest_api import DSPACAPI
from dotenv import load_dotenv

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


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


def login(ds: DSPACAPI, botObj, name, loop, use_email):
try:
# API call to generate the login ticket
if use_email:
ticket_response = ds.generate_login_ticket_email()
else:
ticket_response = ds.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(
ds, 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 OTP code received")
# Login with the OTP code
if use_email:
ticket_response = ds.generate_login_ticket_email(sms_code=otp_code)
else:
ticket_response = ds.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["Data"].get("ticket") is not None
):
ticket = ticket_response['Data']['ticket']
else:
raise Exception(f"Login failed. No ticket generated. Response: {ticket_response}")
# Login with the ticket
login_response = ds.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 OTP login: {e}")
print(traceback.format_exc())
return False


def handle_captcha_and_sms(ds: DSPACAPI, botObj, data, loop, name, use_email):
try:
if data.get('needCaptchaCode', False):
print(f"{name}: CAPTCHA required. Requesting CAPTCHA image...")
sms_response = solve_captcha(ds, botObj, name, loop, use_email)
if not sms_response:
raise Exception("Failure solving CAPTCHA!")
print(f"{name}: CAPTCHA solved. SMS response is: {sms_response}")
else:
print(f"{name}: Requesting SMS code...")
sms_response = send_sms_code(ds, name, use_email)
if not sms_response:
raise Exception("Unable to retrieve sms code!")
print(f"{name}: SMS response is: {sms_response}")
return True
except Exception as e:
print(f"Error in CAPTCHA or SMS: {e}")
print(traceback.format_exc())
return False


def solve_captcha(ds: DSPACAPI, botObj, name, loop, use_email):
try:
captcha_image = ds.request_captcha()
if not captcha_image:
raise Exception("Unable to request CAPTCHA image, aborting...")
# Send the CAPTCHA image to Discord for manual input
print("Sending CAPTCHA to Discord for user 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 = ds.request_email_code(captcha_input=captcha_input)
else:
sms_request_response = ds.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 during CAPTCHA code step: {e}")
print(traceback.format_exc())
return None


def send_sms_code(ds: DSPACAPI, name, use_email, captcha_input=None):
if use_email:
sms_code_response = ds.request_email_code(captcha_input=captcha_input)
else:
sms_code_response = ds.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 dspac_holdings(ds: Brokerage, loop=None):
for key in ds.get_account_numbers():
for account in ds.get_account_numbers(key):
obj: DSPACAPI = ds.get_logged_in_objects(key, "ds")
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"]
ds.set_holdings(key, account, sym, qty, cp)
except Exception as e:
printAndDiscord(f"Error getting DSPAC holdings: {e}")
print(traceback.format_exc())
continue
printHoldings(ds, loop, False)


def dspac_transaction(ds: Brokerage, orderObj: stockOrder, loop=None):
print()
print("==============================")
print("DSPAC")
print("==============================")
print()
for s in orderObj.get_stocks():
for key in ds.get_account_numbers():
action = orderObj.get_action().lower()
printAndDiscord(
f"{key}: {action}ing {orderObj.get_amount()} of {s}",
loop,
)
for account in ds.get_account_numbers(key):
obj: DSPACAPI = ds.get_logged_in_objects(key, "ds")
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
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ asyncio==3.4.3
bbae-invest-api==0.1.3
chaseinvest-api==0.2.5
discord.py==2.4.0
dspac_invest_api==0.1.3
fennel-invest-api==1.1.0
firstrade==0.0.30
GitPython==3.1.43

0 comments on commit e95ab01

Please sign in to comment.