Skip to content

Commit 351a5d7

Browse files
committed
place orders working
1 parent b8904d7 commit 351a5d7

File tree

6 files changed

+204
-5
lines changed

6 files changed

+204
-5
lines changed

.deepsource.toml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
version = 1
2+
3+
[[analyzers]]
4+
name = "python"
5+
6+
[analyzers.meta]
7+
runtime_version = "3.x.x"
8+
9+
[[transformers]]
10+
name = "black"
11+
12+
[[transformers]]
13+
name = "isort"

.github/workflows/pypi_publish.yml

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Public to PyPI
2+
name: Publish to PyPI
3+
on:
4+
push:
5+
paths:
6+
- "setup.py"
7+
8+
jobs:
9+
build:
10+
name: Build
11+
runs-on: ubuntu-latest
12+
environment:
13+
name: "pypi"
14+
url: https://pypi.org/p/fennel-invest-api
15+
permissions:
16+
id-token: write
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
- name: Build and Publish
21+
uses: lsst-sqre/build-and-publish-to-pypi@v2
22+
with:
23+
python-version: "3.11"
24+
upload: ${{ github.ref == 'refs/heads/main' }}

README.md

+71-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,71 @@
1-
# fennel-invest-api
1+
# Unofficial Fennel Invest API
2+
3+
This is an unofficial API for Fennel.com. It is a simple Python wrapper around the Fennel.com GraphQL API. It is not affiliated with Fennel.com in any way.
4+
5+
Fennel does everything via GraphQL, so yes, this is very slow.
6+
7+
This is still a work in progress, so it will have bugs and missing features. Please feel free to contribute!
8+
9+
## Installation
10+
11+
```bash
12+
pip install fennel-invest-api
13+
```
14+
15+
## Usage: Logging In
16+
17+
```python
18+
from fennel_invest_api import Fennel
19+
20+
fennel = Fennel()
21+
fennel.login(
22+
23+
wait_for_2fa=True # When logging in for the first time, you need to wait for email 2FA
24+
)
25+
```
26+
27+
If you'd like to handle the 2FA yourself programmatically instead of waiting for `input()`, you can call it with `wait_for_2fa=False`, catch the 2FA exception, then call it again with the 2FA code:
28+
29+
```python
30+
fennel.login(
31+
32+
wait_for_2fa=False
33+
code="123456" # Should be six-digit integer from email
34+
)
35+
```
36+
37+
## Usage: Get Stock Holdings
38+
```python
39+
positions = fennel.get_stock_holdings()
40+
for position in positions:
41+
print(position)
42+
```
43+
44+
## Usage: Get Portfolio
45+
```python
46+
portfolio = fennel.get_portfolio_summary()
47+
print(portfolio)
48+
```
49+
50+
## Usage: Placing Orders
51+
```python
52+
order = fennel.place_order(
53+
symbol="AAPL",
54+
quantity=1,
55+
side="buy", # Must be "buy" or "sell"
56+
price="market" # Only market orders are supported for now
57+
)
58+
print(order)
59+
```
60+
61+
## Contributing
62+
Found or fixed a bug? Have a feature request? Feel free to open an issue or pull request!
63+
64+
Enjoying the project? Feel free to Sponsor me on GitHub or Ko-fi!
65+
66+
[![Sponsor](https://img.shields.io/badge/sponsor-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#white)](https://github.com/sponsors/NelsonDane)
67+
[![ko-fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white
68+
)](https://ko-fi.com/X8X6LFCI0)
69+
70+
## DISCLAIMER
71+
DISCLAIMER: I am not a financial advisor and not affiliated with Fennel.com. Use this tool at your own risk. I am not responsible for any losses or damages you may incur by using this project. This tool is provided as-is with no warranty.

fennel_invest_api/endpoints.py

+49-2
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ def retrieve_bearer_url(self):
1313
def oauth_url(self):
1414
return f"{self.accounts}/oauth/token"
1515

16-
def build_graphql_payload(self, query):
16+
def build_graphql_payload(self, query, variables={}):
1717
return {
1818
"operationName": None,
19-
"variables": {},
19+
"variables": variables,
2020
"query": query
2121
}
2222

@@ -65,6 +65,53 @@ def stock_holdings_query(self):
6565
"""
6666
return json.dumps(self.build_graphql_payload(query))
6767

68+
def is_market_open_query(self):
69+
query = """
70+
query MarketHours {
71+
securityMarketInfo {
72+
isOpen
73+
}
74+
}
75+
"""
76+
return json.dumps(self.build_graphql_payload(query))
77+
78+
def stock_search_query(self, symbol, count=5):
79+
# idk what count is
80+
query = """
81+
query Search($query: String!, $count: Int) {
82+
searchSearch {
83+
searchSecurities(query: $query, count: $count) {
84+
isin
85+
}
86+
}
87+
}
88+
"""
89+
variables = {
90+
"query": symbol,
91+
"count": count
92+
}
93+
return json.dumps(self.build_graphql_payload(query, variables))
94+
95+
def stock_order_query(self, symbol, quantity, isin, side, priceRule):
96+
query = """
97+
mutation CreateOrder($order_details: OrderDetailsInput__!){
98+
orderCreateOrder(order: $order_details)
99+
}
100+
"""
101+
variables = {
102+
"order_details": {
103+
"quantity": quantity,
104+
"symbol": symbol,
105+
"isin": isin,
106+
"side": side,
107+
"priceRule": priceRule,
108+
"timeInForce": "day",
109+
"routingOption": "exchange_ats_sdp",
110+
}
111+
}
112+
return json.dumps(self.build_graphql_payload(query, variables))
113+
114+
68115
@staticmethod
69116
def build_headers(Bearer, graphql=True):
70117
headers = {

fennel_invest_api/fennel.py

+34-2
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ def login(self, email, wait_for_code=True, code=None):
106106
# refresh_token() # Refresh token after login?
107107
self._save_credentials()
108108
return True
109-
110109

111110
def refresh_token(self):
112111
url = self.endpoints.oauth_url()
@@ -154,4 +153,37 @@ def get_stock_holdings(self):
154153
response = self.session.post(self.endpoints.graphql, headers=headers, data=query)
155154
if response.status_code != 200:
156155
raise Exception(f"Stock Holdings Request failed with status code {response.status_code}: {response.text}")
157-
return response.json()
156+
response = response.json()
157+
return response['data']['portfolio']['bulbs']
158+
159+
@check_login
160+
def is_market_open(self):
161+
query = self.endpoints.is_market_open_query()
162+
headers = self.endpoints.build_headers(self.Bearer)
163+
response = self.session.post(self.endpoints.graphql, headers=headers, data=query)
164+
if response.status_code != 200:
165+
raise Exception(f"Market Open Request failed with status code {response.status_code}: {response.text}")
166+
response = response.json()
167+
return response['data']['securityMarketInfo']['isOpen']
168+
169+
@check_login
170+
def place_order(self, ticker, quantity, side, price="market"):
171+
if side.lower() not in ["buy", "sell"]:
172+
raise Exception("Side must be either 'buy' or 'sell'")
173+
# Check if market is open
174+
if not self.is_market_open():
175+
raise Exception("Market is closed. Cannot place order.")
176+
# Search for stock "isin"
177+
query = self.endpoints.stock_search_query(ticker)
178+
headers = self.endpoints.build_headers(self.Bearer)
179+
search_response = self.session.post(self.endpoints.graphql, headers=headers, data=query)
180+
if search_response.status_code != 200:
181+
raise Exception(f"Stock Search Request failed with status code {search_response.status_code}: {search_response.text}")
182+
search_response = search_response.json()
183+
isin = search_response['data']['searchSearch']['searchSecurities'][0]['isin']
184+
# Place order
185+
query = self.endpoints.stock_order_query(ticker, quantity, isin, side, price)
186+
order_response = self.session.post(self.endpoints.graphql, headers=headers, data=query)
187+
if order_response.status_code != 200:
188+
raise Exception(f"Order Request failed with status code {order_response.status_code}: {order_response.text}")
189+
return order_response.json()

setup.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from setuptools import setup
2+
3+
setup(
4+
name="fennel_invest_api",
5+
version="1.0.0",
6+
description="Unofficial Fennel.com Invest API written in Python Requests",
7+
long_description=open("README.md").read(),
8+
long_description_content_type="text/markdown",
9+
url="https://github.com/NelsonDane/fennel-invest-api",
10+
author="Nelson Dane",
11+
packages=["fennel_invest_api"],
12+
install_requires=["requests", "python-dotenv"],
13+
)

0 commit comments

Comments
 (0)