firstrade 0.0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- firstrade/account.py +220 -0
- firstrade/order.py +141 -0
- firstrade/symbols.py +54 -0
- firstrade/urls.py +45 -0
- firstrade-0.0.1.1.dist-info/LICENSE +18 -0
- firstrade-0.0.1.1.dist-info/METADATA +135 -0
- firstrade-0.0.1.1.dist-info/RECORD +9 -0
- firstrade-0.0.1.1.dist-info/WHEEL +5 -0
- firstrade-0.0.1.1.dist-info/top_level.txt +1 -0
firstrade/account.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import pickle
|
|
3
|
+
import re
|
|
4
|
+
import os
|
|
5
|
+
from time import sleep
|
|
6
|
+
from bs4 import BeautifulSoup
|
|
7
|
+
from firstrade import urls
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FTSession:
|
|
11
|
+
"""
|
|
12
|
+
Class creating a session for Firstrade.
|
|
13
|
+
"""
|
|
14
|
+
def __init__(self, username, password, pin, persistent_session=True):
|
|
15
|
+
"""
|
|
16
|
+
Initializes a new instance of the FTSession class.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
username (str): Firstrade login username.
|
|
20
|
+
password (str): Firstrade login password.
|
|
21
|
+
pin (str): Firstrade login pin.
|
|
22
|
+
persistent_session (bool, optional): Whether the user wants to save the session cookies. Defaults to False.
|
|
23
|
+
"""
|
|
24
|
+
self.username = username
|
|
25
|
+
self.password = password
|
|
26
|
+
self.pin = pin
|
|
27
|
+
self.persistent_session = persistent_session
|
|
28
|
+
self.session = requests.Session()
|
|
29
|
+
self.cookies = {}
|
|
30
|
+
self.login()
|
|
31
|
+
|
|
32
|
+
def login(self):
|
|
33
|
+
"""
|
|
34
|
+
Method to validate and login to the Firstrade platform.
|
|
35
|
+
"""
|
|
36
|
+
headers = urls.session_headers()
|
|
37
|
+
cookies = self.load_cookies()
|
|
38
|
+
cookies = requests.utils.cookiejar_from_dict(cookies)
|
|
39
|
+
self.session.cookies.update(cookies)
|
|
40
|
+
response = self.session.get(
|
|
41
|
+
url=urls.get_xml(), headers=urls.session_headers(), cookies=cookies
|
|
42
|
+
)
|
|
43
|
+
if response.status_code != 200:
|
|
44
|
+
raise Exception('Login failed. Check your credentials or internet connection.')
|
|
45
|
+
if "/cgi-bin/sessionfailed?reason=6" in response.text:
|
|
46
|
+
self.session.get(url=urls.login(), headers=headers)
|
|
47
|
+
data = {
|
|
48
|
+
'redirect': '',
|
|
49
|
+
'ft_locale': 'en-us',
|
|
50
|
+
'login.x': 'Log In',
|
|
51
|
+
'username': r'' + self.username,
|
|
52
|
+
'password': r'' + self.password,
|
|
53
|
+
'destination_page': 'home'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
self.session.post(
|
|
57
|
+
url=urls.login(), headers=headers,
|
|
58
|
+
cookies=self.session.cookies, data=data
|
|
59
|
+
)
|
|
60
|
+
data = {
|
|
61
|
+
'destination_page': 'home',
|
|
62
|
+
'pin': self.pin,
|
|
63
|
+
'pin.x': '++OK++',
|
|
64
|
+
'sring': '0',
|
|
65
|
+
'pin': self.pin
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
self.session.post(
|
|
69
|
+
url=urls.pin(), headers=headers,
|
|
70
|
+
cookies=self.session.cookies, data=data
|
|
71
|
+
)
|
|
72
|
+
if self.persistent_session:
|
|
73
|
+
self.save_cookies()
|
|
74
|
+
self.cookies = self.session.cookies
|
|
75
|
+
if "/cgi-bin/sessionfailed?reason=6" in self.session.get(
|
|
76
|
+
url=urls.get_xml(), headers=urls.session_headers(), cookies=cookies
|
|
77
|
+
).text:
|
|
78
|
+
raise Exception('Login failed. Check your credentials.')
|
|
79
|
+
|
|
80
|
+
def load_cookies(self):
|
|
81
|
+
"""
|
|
82
|
+
Checks if session cookies were saved.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Dict: Dictionary of cookies. Nom Nom
|
|
86
|
+
"""
|
|
87
|
+
cookies = {}
|
|
88
|
+
for filename in os.listdir('.'):
|
|
89
|
+
if filename.endswith(f'{self.username}.pkl'):
|
|
90
|
+
with open(filename, 'rb') as f:
|
|
91
|
+
cookies = pickle.load(f)
|
|
92
|
+
return cookies
|
|
93
|
+
|
|
94
|
+
def save_cookies(self):
|
|
95
|
+
"""
|
|
96
|
+
Saves session cookies to a file.
|
|
97
|
+
"""
|
|
98
|
+
with open(f'ft_cookies{self.username}.pkl', 'wb') as f:
|
|
99
|
+
pickle.dump(self.session.cookies.get_dict(), f)
|
|
100
|
+
|
|
101
|
+
def __getattr__(self, name):
|
|
102
|
+
"""
|
|
103
|
+
Forwards unknown attribute access to session object.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
name (str): The name of the attribute to be accessed.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The value of the requested attribute from the session object.
|
|
110
|
+
"""
|
|
111
|
+
return getattr(self.session, name)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class FTAccountData:
|
|
115
|
+
"""
|
|
116
|
+
Dataclass for storing account information.
|
|
117
|
+
"""
|
|
118
|
+
def __init__(self, session):
|
|
119
|
+
"""
|
|
120
|
+
Initializes a new instance of the FTAccountData class.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
session (requests.Session): The session object used for making HTTP requests.
|
|
124
|
+
"""
|
|
125
|
+
self.session = session
|
|
126
|
+
self.cookies = self.session.cookies
|
|
127
|
+
self.html_string = ''
|
|
128
|
+
self.all_accounts = []
|
|
129
|
+
self.account_numbers = []
|
|
130
|
+
self.account_statuses = []
|
|
131
|
+
self.account_balances = []
|
|
132
|
+
self.securities_held = {}
|
|
133
|
+
all_account_info = []
|
|
134
|
+
self.html_string = self.session.get(
|
|
135
|
+
url=urls.account_list(),
|
|
136
|
+
headers=urls.session_headers(),
|
|
137
|
+
cookies=self.cookies
|
|
138
|
+
).text
|
|
139
|
+
regex_accounts = re.findall(
|
|
140
|
+
r"([0-9]+)-", self.html_string
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
for match in regex_accounts:
|
|
144
|
+
self.account_numbers.append(match)
|
|
145
|
+
|
|
146
|
+
for account in self.account_numbers:
|
|
147
|
+
data = {'accountId': account}
|
|
148
|
+
self.session.post(
|
|
149
|
+
url=urls.account_status(),
|
|
150
|
+
headers=urls.session_headers(),
|
|
151
|
+
cookies=self.session.cookies,
|
|
152
|
+
data=data
|
|
153
|
+
)
|
|
154
|
+
sleep(1)
|
|
155
|
+
data = {
|
|
156
|
+
'page': 'bal',
|
|
157
|
+
'account_id': account
|
|
158
|
+
}
|
|
159
|
+
account_soup = BeautifulSoup(self.session.post(
|
|
160
|
+
url=urls.get_xml(),
|
|
161
|
+
headers=urls.session_headers(),
|
|
162
|
+
cookies=self.session.cookies,
|
|
163
|
+
data=data,
|
|
164
|
+
).text, 'xml')
|
|
165
|
+
balance = account_soup.find('total_account_value').text
|
|
166
|
+
self.account_balances.append(balance)
|
|
167
|
+
data = { 'req': 'get_status'}
|
|
168
|
+
account_status = self.session.post(
|
|
169
|
+
url=urls.status(),
|
|
170
|
+
headers=urls.session_headers(),
|
|
171
|
+
cookies=self.session.cookies,
|
|
172
|
+
data=data
|
|
173
|
+
).json()
|
|
174
|
+
self.account_statuses.append(account_status['data'])
|
|
175
|
+
|
|
176
|
+
all_account_info.append({account:
|
|
177
|
+
{'Balance': balance, 'Status':
|
|
178
|
+
{'primary':account_status['data']['primary'], 'domestic':account_status['data']['domestic'],
|
|
179
|
+
'joint':account_status['data']['joint'], 'ira':account_status['data']['ira'],
|
|
180
|
+
'hasMargin':account_status['data']['hasMargin'], 'opLevel':account_status['data']['opLevel'], 'p_country':account_status['data']['p_country'],
|
|
181
|
+
'mrgnStatus':account_status['data']['mrgnStatus'], 'opStatus':account_status['data']['opStatus'], 'margin_id':account_status['data']['margin_id']
|
|
182
|
+
}
|
|
183
|
+
}})
|
|
184
|
+
|
|
185
|
+
self.all_accounts = all_account_info
|
|
186
|
+
|
|
187
|
+
def get_positions(self, account):
|
|
188
|
+
"""Gets currently held positions for a given account.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
account (str): Account number of the account you want to get positions for.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
self.securities_held {dict}: Dict of held positions with the pos. ticker as the key.
|
|
195
|
+
"""
|
|
196
|
+
data = {
|
|
197
|
+
'page': 'pos',
|
|
198
|
+
'accountId': str(account),
|
|
199
|
+
}
|
|
200
|
+
position_soup = BeautifulSoup(self.session.post(
|
|
201
|
+
url=urls.get_xml(),
|
|
202
|
+
headers=urls.session_headers(),
|
|
203
|
+
data=data,
|
|
204
|
+
cookies=self.cookies
|
|
205
|
+
).text, 'xml')
|
|
206
|
+
|
|
207
|
+
tickers = position_soup.find_all('symbol')
|
|
208
|
+
quantity = position_soup.find_all('quantity')
|
|
209
|
+
price = position_soup.find_all('price')
|
|
210
|
+
change = position_soup.find_all('change')
|
|
211
|
+
change_percent = position_soup.find_all('changepercent')
|
|
212
|
+
vol = position_soup.find_all('vol')
|
|
213
|
+
for i, ticker in enumerate(tickers):
|
|
214
|
+
ticker = ticker.text
|
|
215
|
+
self.securities_held[ticker] = {
|
|
216
|
+
'quantity': quantity[i].text,
|
|
217
|
+
'price': price[i].text, 'change': change[i].text,
|
|
218
|
+
'change_percent': change_percent[i].text, 'vol': vol[i].text
|
|
219
|
+
}
|
|
220
|
+
return self.securities_held
|
firstrade/order.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from firstrade.account import FTSession
|
|
2
|
+
from firstrade import urls
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from bs4 import BeautifulSoup
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PriceType(str, Enum):
|
|
8
|
+
"""
|
|
9
|
+
This is an :class: 'enum.Enum' that contains the valid price types for an order.
|
|
10
|
+
"""
|
|
11
|
+
LIMIT = '2'
|
|
12
|
+
MARKET = '1'
|
|
13
|
+
STOP = '3'
|
|
14
|
+
STOP_LIMIT = '4'
|
|
15
|
+
TRAILING_STOP_DOLLAR = '5'
|
|
16
|
+
TRAILING_STOP_PERCENT = '6'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Duration(str, Enum):
|
|
20
|
+
"""
|
|
21
|
+
This is an :class:'~enum.Enum' that contains the valid durations for an order.
|
|
22
|
+
"""
|
|
23
|
+
DAY = '0'
|
|
24
|
+
GT90 = '1'
|
|
25
|
+
PRE_MARKET = 'A'
|
|
26
|
+
AFTER_MARKET = 'P'
|
|
27
|
+
DAY_EXT = 'D'
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OrderType(str, Enum):
|
|
31
|
+
"""
|
|
32
|
+
This is an :class:'~enum.Enum' that contains the valid order types for an order.
|
|
33
|
+
"""
|
|
34
|
+
BUY = 'B'
|
|
35
|
+
SELL = 'S'
|
|
36
|
+
SELL_SHORT = 'SS'
|
|
37
|
+
BUY_TO_COVER = 'BC'
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Order:
|
|
41
|
+
"""
|
|
42
|
+
This class contains information about an order. It also contains a method to place an order.
|
|
43
|
+
"""
|
|
44
|
+
def __init__(self, ft_session: FTSession):
|
|
45
|
+
self.ft_session = ft_session
|
|
46
|
+
self.order_confirmation = {}
|
|
47
|
+
|
|
48
|
+
def place_order(
|
|
49
|
+
self, account, symbol, price_type: PriceType, order_type: OrderType,
|
|
50
|
+
quantity, duration: Duration, price=0.00, dry_run=True
|
|
51
|
+
):
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
Builds and places an order.
|
|
55
|
+
:attr: 'order_confirmation` contains the order confirmation data after order placement.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
account (str): Account number of the account to place the order in.
|
|
59
|
+
symbol (str): Ticker to place the order for.
|
|
60
|
+
order_type (PriceType): Price Type i.e. LIMIT, MARKET, STOP, etc.
|
|
61
|
+
quantity (float): The number of shares to buy.
|
|
62
|
+
duration (Duration): Duration of the order i.e. DAY, GT90, etc.
|
|
63
|
+
price (float, optional): The price to buy the shares at. Defaults to 0.00.
|
|
64
|
+
dry_run (bool, optional): Whether you want the order to be placed or not.
|
|
65
|
+
Defaults to True.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Order:order_confirmation: Dictionary containing the order confirmation data.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
if dry_run:
|
|
72
|
+
previewOrders = '1'
|
|
73
|
+
else:
|
|
74
|
+
previewOrders = ''
|
|
75
|
+
|
|
76
|
+
if price_type == PriceType.MARKET:
|
|
77
|
+
price = ''
|
|
78
|
+
|
|
79
|
+
data = {
|
|
80
|
+
'submiturl': '/cgi-bin/orderbar',
|
|
81
|
+
'orderbar_clordid': '',
|
|
82
|
+
'orderbar_accountid': '',
|
|
83
|
+
'stockorderpage': 'yes',
|
|
84
|
+
'submitOrders': '1',
|
|
85
|
+
'previewOrders': previewOrders,
|
|
86
|
+
'lotMethod': '1',
|
|
87
|
+
'accountType': '1',
|
|
88
|
+
'quoteprice': '',
|
|
89
|
+
'viewederror': '',
|
|
90
|
+
'stocksubmittedcompanyname1': '',
|
|
91
|
+
'accountId': account,
|
|
92
|
+
'transactionType': order_type,
|
|
93
|
+
'quantity': quantity,
|
|
94
|
+
'symbol': symbol,
|
|
95
|
+
'priceType': price_type,
|
|
96
|
+
'limitPrice': price,
|
|
97
|
+
'duration': duration,
|
|
98
|
+
'qualifier': '0',
|
|
99
|
+
'cond_symbol0_0': '',
|
|
100
|
+
'cond_type0_0': '2',
|
|
101
|
+
'cond_compare_type0_0': '2',
|
|
102
|
+
'cond_compare_value0_0': '',
|
|
103
|
+
'cond_and_or0': '1',
|
|
104
|
+
'cond_symbol0_1': '',
|
|
105
|
+
'cond_type0_1': '2',
|
|
106
|
+
'cond_compare_type0_1': '2',
|
|
107
|
+
'cond_compare_value0_1': ''
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
order_data = BeautifulSoup(self.ft_session.post(
|
|
111
|
+
url=urls.orderbar(),
|
|
112
|
+
headers=urls.session_headers(),
|
|
113
|
+
data=data
|
|
114
|
+
).text, 'xml')
|
|
115
|
+
order_confirmation = {}
|
|
116
|
+
order_success = order_data.find('success').text.strip()
|
|
117
|
+
order_confirmation['success'] = order_success
|
|
118
|
+
action_data = order_data.find('actiondata').text.strip()
|
|
119
|
+
if order_success != "No":
|
|
120
|
+
# Extract the table data
|
|
121
|
+
table_start = action_data.find('<table')
|
|
122
|
+
table_end = action_data.find('</table>') + len('</table>')
|
|
123
|
+
table_data = action_data[table_start:table_end]
|
|
124
|
+
table_data = BeautifulSoup(table_data, 'xml')
|
|
125
|
+
titles = table_data.find_all('th')
|
|
126
|
+
data = table_data.find_all('td')
|
|
127
|
+
for i, title in enumerate(titles):
|
|
128
|
+
order_confirmation[f'{title.get_text()}'] = data[i].get_text()
|
|
129
|
+
if not dry_run:
|
|
130
|
+
start_index = action_data.find('Your order reference number is: ') + len('Your order reference number is: ')
|
|
131
|
+
end_index = action_data.find('</div>', start_index)
|
|
132
|
+
order_number = action_data[start_index:end_index]
|
|
133
|
+
else:
|
|
134
|
+
start_index = action_data.find('id="') + len('id="')
|
|
135
|
+
end_index = action_data.find('" style=', start_index)
|
|
136
|
+
order_number = action_data[start_index:end_index]
|
|
137
|
+
order_confirmation['orderid'] = order_number
|
|
138
|
+
else:
|
|
139
|
+
order_confirmation['actiondata'] = action_data
|
|
140
|
+
order_confirmation['errcode'] = order_data.find('errcode').text.strip()
|
|
141
|
+
self.order_confirmation = order_confirmation
|
firstrade/symbols.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from bs4 import BeautifulSoup
|
|
2
|
+
from firstrade.account import FTSession
|
|
3
|
+
from firstrade import urls
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SymbolQuote:
|
|
7
|
+
"""
|
|
8
|
+
Dataclass containing quote information for a symbol.
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
ft_session (FTSession): The session object used for making HTTP requests to Firstrade.
|
|
12
|
+
symbol (str): The symbol for which the quote information is retrieved.
|
|
13
|
+
exchange (str): The exchange where the symbol is traded.
|
|
14
|
+
bid (float): The bid price for the symbol.
|
|
15
|
+
ask (float): The ask price for the symbol.
|
|
16
|
+
last (float): The last traded price for the symbol.
|
|
17
|
+
change (float): The change in price for the symbol.
|
|
18
|
+
high (float): The highest price for the symbol during the trading day.
|
|
19
|
+
low (float): The lowest price for the symbol during the trading day.
|
|
20
|
+
volume (str): The volume of shares traded for the symbol.
|
|
21
|
+
company_name (str): The name of the company associated with the symbol.
|
|
22
|
+
"""
|
|
23
|
+
def __init__(self, ft_session: FTSession, symbol: str):
|
|
24
|
+
"""
|
|
25
|
+
Initializes a new instance of the SymbolQuote class.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
ft_session (FTSession): The session object used for making HTTP requests to Firstrade.
|
|
29
|
+
symbol (str): The symbol for which the quote information is retrieved.
|
|
30
|
+
"""
|
|
31
|
+
self.ft_session = ft_session
|
|
32
|
+
self.symbol = symbol
|
|
33
|
+
symbol_data = self.ft_session.get(
|
|
34
|
+
url=urls.quote(self.symbol),
|
|
35
|
+
headers=urls.session_headers()
|
|
36
|
+
)
|
|
37
|
+
soup = BeautifulSoup(symbol_data.text, 'xml')
|
|
38
|
+
quote = soup.find('quote')
|
|
39
|
+
self.symbol = quote.find('symbol').text
|
|
40
|
+
self.exchange = quote.find('exchange').text
|
|
41
|
+
self.bid = float(quote.find('bid').text)
|
|
42
|
+
self.ask = float(quote.find('ask').text)
|
|
43
|
+
self.last = float(quote.find('last').text)
|
|
44
|
+
self.change = float(quote.find('change').text)
|
|
45
|
+
if quote.find('high').text == 'N/A':
|
|
46
|
+
self.high = None
|
|
47
|
+
else:
|
|
48
|
+
self.high = float(quote.find('high').text)
|
|
49
|
+
if quote.find('low').text == 'N/A':
|
|
50
|
+
self.low = 'None'
|
|
51
|
+
else:
|
|
52
|
+
self.low = float(quote.find('low').text)
|
|
53
|
+
self.volume = quote.find('vol').text
|
|
54
|
+
self.company_name = quote.find('companyname').text
|
firstrade/urls.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
def get_xml():
|
|
4
|
+
return 'https://invest.firstrade.com/cgi-bin/getxml'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def login():
|
|
8
|
+
return 'https://invest.firstrade.com/cgi-bin/login'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def pin():
|
|
12
|
+
return 'https://invest.firstrade.com/cgi-bin/enter_pin?destination_page=home'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def account_list():
|
|
16
|
+
return 'https://invest.firstrade.com/cgi-bin/getaccountlist'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def quote(symbol):
|
|
20
|
+
return f'https://invest.firstrade.com/cgi-bin/getxml?page=quo"eSymbol={symbol}'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def orderbar():
|
|
24
|
+
return 'https://invest.firstrade.com/cgi-bin/orderbar'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def account_status():
|
|
28
|
+
return 'https://invest.firstrade.com/cgi-bin/account_status'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def status():
|
|
32
|
+
return "https://invest.firstrade.com/scripts/profile/margin_v2.php"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def session_headers():
|
|
36
|
+
headers = {
|
|
37
|
+
'Accept': '*/*',
|
|
38
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
39
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
40
|
+
'Host': 'invest.firstrade.com',
|
|
41
|
+
'Referer': 'https://invest.firstrade.com/cgi-bin/main',
|
|
42
|
+
'Connection': 'keep-alive',
|
|
43
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.81'
|
|
44
|
+
}
|
|
45
|
+
return headers
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Copyright <2023> <MAXXRK>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the “Software”), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
14
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
16
|
+
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
17
|
+
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
18
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: firstrade
|
|
3
|
+
Version: 0.0.1.1
|
|
4
|
+
Summary: An unofficial API for Firstrade
|
|
5
|
+
Home-page: https://github.com/MaxxRK/firstrade-api
|
|
6
|
+
Download-URL: https://github.com/MaxxRK/firstrade-api/archive/refs/tags/0.0.1.1.tar.gz
|
|
7
|
+
Author: MaxxRK
|
|
8
|
+
Author-email: maxxrk@pm.me
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: FIRSTRADE,API
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Session
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: requests
|
|
23
|
+
Requires-Dist: beautifulsoup4
|
|
24
|
+
Requires-Dist: lxml
|
|
25
|
+
|
|
26
|
+
# firstrade-api
|
|
27
|
+
A reverse-engineered python API to interact with the Firstrade Trading platform.
|
|
28
|
+
|
|
29
|
+
This is not an official api! This api's functionality may change at any time.
|
|
30
|
+
|
|
31
|
+
This api provides a means of buying and selling stocks through Firstrade. It uses the Session class from requests to get authorization cookies. The rest is done with reverse engineered requests to Firstrade's API.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Contribution
|
|
36
|
+
I am new to coding and new to open-source. I would love any help and suggestions!
|
|
37
|
+
|
|
38
|
+
## Setup
|
|
39
|
+
Install using pypi:
|
|
40
|
+
```
|
|
41
|
+
pip install firstrade
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quikstart
|
|
45
|
+
The code below will:
|
|
46
|
+
- Login and print account info.
|
|
47
|
+
- Get a quote for 'INTC' and print out the information
|
|
48
|
+
- Place a market order for 'INTC' on the first account in the `account_numbers` list
|
|
49
|
+
- Print out the order confirmation
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
from firstrade import account
|
|
53
|
+
from firstrade import symbols
|
|
54
|
+
from firstrade import order
|
|
55
|
+
|
|
56
|
+
# Create a session
|
|
57
|
+
ft_ss = account.FTSession(username='', password='', pin='')
|
|
58
|
+
|
|
59
|
+
# Get account data
|
|
60
|
+
ft_accounts = account.FTAccountData(ft_ss)
|
|
61
|
+
if len(ft_accounts.account_numbers) < 1:
|
|
62
|
+
raise Exception('No accounts found or an error occured exiting...')
|
|
63
|
+
|
|
64
|
+
# Print ALL account data
|
|
65
|
+
print(ft_accounts.all_accounts)
|
|
66
|
+
|
|
67
|
+
# Print 1st account number.
|
|
68
|
+
print(ft_accounts.account_numbers[0])
|
|
69
|
+
|
|
70
|
+
# Print ALL accounts market values.
|
|
71
|
+
print(ft_accounts.account_balances)
|
|
72
|
+
|
|
73
|
+
# Get quote for INTC
|
|
74
|
+
quote = symbols.SymbolQuote(ft_ss, 'INTC')
|
|
75
|
+
print(f"Symbol: {quote.symbol}")
|
|
76
|
+
print(f"Exchange: {quote.exchange}")
|
|
77
|
+
print(f"Bid: {quote.bid}")
|
|
78
|
+
print(f"Ask: {quote.ask}")
|
|
79
|
+
print(f"Last: {quote.last}")
|
|
80
|
+
print(f"Change: {quote.change}")
|
|
81
|
+
print(f"High: {quote.high}")
|
|
82
|
+
print(f"Low: {quote.low}")
|
|
83
|
+
print(f"Volume: {quote.volume}")
|
|
84
|
+
print(f"Company Name: {quote.company_name}")
|
|
85
|
+
|
|
86
|
+
# Get positions and print them out for an account.
|
|
87
|
+
positions = ft_accounts.get_positions(account=ft_accounts.account_numbers[1])
|
|
88
|
+
for key in ft_accounts.securities_held:
|
|
89
|
+
print(f"Quantity {ft_accounts.securities_held[key]['quantity']} of security {key} held in account {ft_accounts.account_numbers[1]}")
|
|
90
|
+
|
|
91
|
+
# Create an order object.
|
|
92
|
+
ft_order = order.Order(ft_ss)
|
|
93
|
+
|
|
94
|
+
# Place order and print out order confirmation data.
|
|
95
|
+
ft_order.place_order(
|
|
96
|
+
ft_accounts.account_numbers[0],
|
|
97
|
+
symbol='INTC',
|
|
98
|
+
price_type=order.PriceType.MARKET,
|
|
99
|
+
order_type=order.OrderType.BUY,
|
|
100
|
+
quantity=1,
|
|
101
|
+
duration=order.Duration.DAY,
|
|
102
|
+
dry_run=True
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Print Order data Dict
|
|
106
|
+
print(ft_order.order_confirmation)
|
|
107
|
+
|
|
108
|
+
# Check if order was successful
|
|
109
|
+
if ft_order.order_confirmation['success'] == 'Yes':
|
|
110
|
+
print('Order placed successfully.')
|
|
111
|
+
# Print Order ID
|
|
112
|
+
print(f"Order ID: {ft_order.order_confirmation['orderid']}.")
|
|
113
|
+
else:
|
|
114
|
+
print('Failed to place order.')
|
|
115
|
+
# Print errormessage
|
|
116
|
+
print(ft_order.order_confirmation['actiondata'])
|
|
117
|
+
```
|
|
118
|
+
This code is also in test.py
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Implemented Features
|
|
123
|
+
- [x] Login
|
|
124
|
+
- [x] Get Quotes
|
|
125
|
+
- [x] Get Account Data
|
|
126
|
+
- [x] Place Orders and Receive order confirmation
|
|
127
|
+
- [x] Get Currently Held Positions
|
|
128
|
+
|
|
129
|
+
## TO DO
|
|
130
|
+
- [ ] Check on placed order status.
|
|
131
|
+
- [ ] Cancel placed orders
|
|
132
|
+
- [ ] Options
|
|
133
|
+
- [ ] Give me some Ideas!
|
|
134
|
+
|
|
135
|
+
[](https://ko-fi.com/O5O6PTOYG)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
firstrade/account.py,sha256=Vu7kQ4JxMo_Br-5EVVoj20pyWy51RBtyJETx5FGPYLc,7836
|
|
2
|
+
firstrade/order.py,sha256=BmwPSb-kxBGBkZZ8ArbJmZOTx1z-Fq0y8-GbF2pDPAA,4992
|
|
3
|
+
firstrade/symbols.py,sha256=4Uh8DjKhTSmQuR7KKp0OrdPVTBgr2e7k5EsmdNoVDPU,2297
|
|
4
|
+
firstrade/urls.py,sha256=W6iNNypdjUXTjS9pNq74FxkjNJUnCeiXZ-C-mRpMyXY,1168
|
|
5
|
+
firstrade-0.0.1.1.dist-info/LICENSE,sha256=wPEQjDqm5zMBmEcZp219Labmq_YIjhudpZiUzyVKaFA,1057
|
|
6
|
+
firstrade-0.0.1.1.dist-info/METADATA,sha256=LtS9ifSKYZOMwyWItSnFhB7k1wY4bNOFX8nxMRDWKV8,4066
|
|
7
|
+
firstrade-0.0.1.1.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
|
|
8
|
+
firstrade-0.0.1.1.dist-info/top_level.txt,sha256=tdA8v-KDxU1u4VV6soiNWGBlni4ojv_t_j2wFn5nZcs,10
|
|
9
|
+
firstrade-0.0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
firstrade
|