initial commit
This commit is contained in:
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
10
.idea/RicoToGhostfolio.iml
generated
Normal file
10
.idea/RicoToGhostfolio.iml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.12 (RicoToGhostfolio)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
6
.idea/misc.xml
generated
Normal file
6
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.12 (RicoToGhostfolio)" />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/RicoToGhostfolio.iml" filepath="$PROJECT_DIR$/.idea/RicoToGhostfolio.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
# Use the official Python image
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
COPY ./requirements.txt /code/requirements.txt
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
||||
|
||||
COPY . /code/app
|
||||
|
||||
CMD ["fastapi", "run", "app/main.py", "--port", "8000"]
|
||||
112
converter.py
Normal file
112
converter.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import io
|
||||
import logging
|
||||
import simplejson as json
|
||||
import pandas as pd
|
||||
import mappings
|
||||
import ghostfolio_service
|
||||
import datetime
|
||||
from correpy.domain.enums import TransactionType
|
||||
from correpy.parsers.brokerage_notes.parser_factory import ParserFactory
|
||||
from fastapi import UploadFile
|
||||
|
||||
account_id = '1590f1bb-b3ee-4dc8-ac46-bd1f15a83e87'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def read_brokerage_pdf(file: UploadFile):
|
||||
content = io.BytesIO(file.file.read())
|
||||
content.seek(0)
|
||||
|
||||
logger.info(f'Processing {file.filename} ')
|
||||
|
||||
try:
|
||||
brokerage_notes = ParserFactory(brokerage_note=content, password='052').parse()
|
||||
return convert_to_ghostfolio(brokerage_notes, file.filename)
|
||||
except Exception as e:
|
||||
print(f'Failed to parse {file.filename}: {e}')
|
||||
logger.error(f'Failed to parse {file.filename}: {e}')
|
||||
return 500
|
||||
|
||||
|
||||
def convert_to_ghostfolio(brokerage_list: list, filename: str):
|
||||
activities = {}
|
||||
|
||||
for brokerage_note in brokerage_list:
|
||||
reference_date = brokerage_note.reference_date.isoformat() + 'T00:00:00.000Z'
|
||||
fees = (brokerage_note.settlement_fee + brokerage_note.emoluments + brokerage_note.others) / len(
|
||||
brokerage_note.transactions)
|
||||
|
||||
# activities['meta'] = {'date': datetime.datetime.now().isoformat(), 'version': 'v0'}
|
||||
activities['activities'] = []
|
||||
# activities['updateCashBalance'] = 'false'
|
||||
|
||||
for transaction in brokerage_note.transactions:
|
||||
|
||||
# activity['fee'] = (brokerage_note.settlement_fee + brokerage_note.emoluments)
|
||||
|
||||
transaction_type: str = ''
|
||||
|
||||
if transaction.transaction_type == TransactionType.BUY:
|
||||
transaction_type = 'BUY'
|
||||
elif transaction.transaction_type == TransactionType.SELL:
|
||||
transaction_type = 'SELL'
|
||||
|
||||
activity = {'accountId': account_id, 'comment': filename, 'date': reference_date,
|
||||
'type': transaction_type,
|
||||
'fee': fees, 'unitPrice': transaction.unit_price, 'quantity': transaction.amount,
|
||||
'symbol': mappings.name_to_tickers[transaction.security.name.strip()].strip(),
|
||||
'currency': 'BRL',
|
||||
'dataSource': 'YAHOO'}
|
||||
activities['activities'].append(activity)
|
||||
|
||||
if len(activity['symbol']) < 1:
|
||||
logger.info(f'No symbol for {transaction.security.name}')
|
||||
|
||||
result = ghostfolio_service.import_activities(activities, filename)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def read_dividends_xlsx(file: UploadFile):
|
||||
dividends_xlsx = file.file
|
||||
df = pd.read_excel(dividends_xlsx, engine='openpyxl')
|
||||
path = f'./{file.filename}'
|
||||
df.to_csv(path, encoding='utf-8')
|
||||
df = pd.read_csv(path, encoding='utf-8')
|
||||
df = df[df['Produto'].notnull()]
|
||||
df = df[df['Tipo de Evento'] != 'PAGAMENTO DE JUROS']
|
||||
|
||||
return dividends_df_to_ghostfolio(df, file.filename)
|
||||
|
||||
|
||||
def dividends_df_to_ghostfolio(dividends_df: pd.DataFrame, filename: str):
|
||||
activities = {"activities": []}
|
||||
|
||||
for index, row in dividends_df.iterrows():
|
||||
payment_date = datetime.datetime.strptime(row['Pagamento'], "%d/%m/%Y").isoformat()
|
||||
name = row['Produto'].split('-')[0].strip() + '.SA'
|
||||
|
||||
if name in mappings.ticker_changes:
|
||||
symbol = mappings.ticker_changes[name]
|
||||
else:
|
||||
symbol = name
|
||||
|
||||
activity = {
|
||||
"accountId": account_id,
|
||||
"date": payment_date,
|
||||
"comment": filename,
|
||||
"currency": "BRL",
|
||||
"dataSource": "YAHOO",
|
||||
"quantity": row['Quantidade'],
|
||||
"type": "DIVIDEND",
|
||||
"symbol": symbol,
|
||||
"unitPrice": row['Preço unitário'],
|
||||
"fee": 0
|
||||
}
|
||||
|
||||
activities["activities"].append(activity)
|
||||
|
||||
print(activities)
|
||||
|
||||
return ghostfolio_service.import_activities(activities, filename)
|
||||
6
docker-compose.yml
Normal file
6
docker-compose.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
services:
|
||||
rico-to-ghostfolio:
|
||||
ports:
|
||||
- "8001:8000"
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
40
ghostfolio_service.py
Normal file
40
ghostfolio_service.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import simplejson as json
|
||||
import ntfy_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_bearer_token():
|
||||
response = requests.post('http://192.168.1.202:3333/api/v1/auth/anonymous', data={
|
||||
"accessToken": "cab07a6b0a87711013ee5457411a26c7c7dd2787830b64b914d5678d4dc54af911752975380467fd73c3798b043855520ab148a372d8fd859860703833e96cba"}
|
||||
)
|
||||
|
||||
token = response.json()['authToken']
|
||||
|
||||
return token
|
||||
|
||||
|
||||
def import_activities(activities_json, filename: str):
|
||||
bearer_token = get_bearer_token()
|
||||
|
||||
data = json.dumps(activities_json)
|
||||
|
||||
response = requests.post('http://192.168.1.202:3333/api/v1/import', data=data,
|
||||
headers={"Authorization": "Bearer " + bearer_token, "Content-Type": "application/json"})
|
||||
|
||||
print(response.json())
|
||||
|
||||
if len(response.json()['activities']) < 1:
|
||||
logger.info(f'No activity imported for {filename}')
|
||||
print(f'No activity imported for {filename}')
|
||||
ntfy_service.send_notification(f'No activity imported for {filename}')
|
||||
else:
|
||||
ntfy_service.send_notification(f'Imported {len(response.json()['activities'])} activities from {filename}')
|
||||
print(f'Imported {len(response.json()['activities'])} activities from {filename}')
|
||||
|
||||
# print(json.dumps(response.json(), indent=4))
|
||||
|
||||
return response.status_code
|
||||
30
main.py
Normal file
30
main.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import logging
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Form, UploadFile, File
|
||||
import ntfy_service
|
||||
|
||||
from converter import read_brokerage_pdf, read_dividends_xlsx
|
||||
import io
|
||||
app = FastAPI()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(filename='rico-to-ghostfolio.log', level=logging.INFO)
|
||||
|
||||
@app.post("/brokerage")
|
||||
async def convert_pdf(file: UploadFile = File(...)):
|
||||
return read_brokerage_pdf(file)
|
||||
|
||||
@app.post("/b3dividends")
|
||||
async def convert_pdf(file: UploadFile = File(...)):
|
||||
return read_dividends_xlsx(file)
|
||||
|
||||
@app.post("/notification")
|
||||
async def notification(request: Request):
|
||||
message = await request.body()
|
||||
return ntfy_service.send_notification(message.decode('utf8'))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
16
mappings.py
Normal file
16
mappings.py
Normal file
@@ -0,0 +1,16 @@
|
||||
name_to_tickers = {
|
||||
"BRASIL ON NM" : 'BBAS3.SA',
|
||||
"BRASIL ON EJ NM": 'BBAS3.SA',
|
||||
"METAL LEVE ON NM": 'LEVE3.SA',
|
||||
"METAL LEVE ON ED NM": 'LEVE3.SA',
|
||||
"TRAN PAULIST PN N1": 'ISAE4.SA',
|
||||
"ISA ENERGIA PN N1": 'ISAE4.SA',
|
||||
"TAESA PN N2": 'TAEE4.SA',
|
||||
"VALE ON NM": 'VALE3.SA',
|
||||
"OI ON N1": 'OIBR3.SA'
|
||||
}
|
||||
|
||||
|
||||
ticker_changes = {
|
||||
"TRPL4.SA": 'ISAE4.SA',
|
||||
}
|
||||
BIN
notas-teste/XPINC_NOTA_NEGOCIACAO_B3_1_10_2024.pdf
Normal file
BIN
notas-teste/XPINC_NOTA_NEGOCIACAO_B3_1_10_2024.pdf
Normal file
Binary file not shown.
6
ntfy_service.py
Normal file
6
ntfy_service.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import requests
|
||||
|
||||
|
||||
def send_notification(message: str):
|
||||
response = requests.post('https://ntfy.ccalifice.com/ghostfolio', data=message, headers={"Authorization": "Bearer tk_dl5jaui6j3pc2dx9rx8qj9sdwh23k"})
|
||||
return response.status_code
|
||||
36
playwright_t.py
Normal file
36
playwright_t.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from playwright.sync_api import sync_playwright
|
||||
import time
|
||||
|
||||
with sync_playwright() as p:
|
||||
# Launch a browser
|
||||
browser = p.chromium.launch(headless=False) # Set headless=True if you don't want to see the browser
|
||||
page = browser.new_page()
|
||||
|
||||
# Go to the login page
|
||||
page.goto("https://www.investidor.b3.com.br/login")
|
||||
|
||||
# Fill in the username and password
|
||||
page.fill('input[name="investidor"]', '01783945052')
|
||||
page.click('button[type="submit"]')
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
page.fill('input[type="password"]', 'malvado6696')
|
||||
|
||||
time.sleep(200)
|
||||
page.click('input[type="checkbox"]')
|
||||
page.click('button[type="submit"]')
|
||||
|
||||
|
||||
# Wait for navigation or any element that confirms login success
|
||||
page.wait_for_load_state('networkidle')
|
||||
|
||||
# Go to the protected page
|
||||
page.goto("https://example.com/secret-page")
|
||||
|
||||
# Get the page content
|
||||
content = page.content()
|
||||
print(content)
|
||||
|
||||
# Close the browser
|
||||
#browser.close()
|
||||
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
11
test_main.http
Normal file
11
test_main.http
Normal file
@@ -0,0 +1,11 @@
|
||||
# Test your FastAPI endpoints
|
||||
|
||||
GET http://127.0.0.1:8000/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
GET http://127.0.0.1:8000/hello/User
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
Reference in New Issue
Block a user