initial commit

This commit is contained in:
2025-11-09 12:26:46 -03:00
commit b305173b9d
17 changed files with 307 additions and 0 deletions

0
.gitignore vendored Normal file
View File

8
.idea/.gitignore generated vendored Normal file
View 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
View 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>

View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,6 @@
services:
rico-to-ghostfolio:
ports:
- "8001:8000"
build:
dockerfile: Dockerfile

40
ghostfolio_service.py Normal file
View 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
View 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
View 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',
}

Binary file not shown.

6
ntfy_service.py Normal file
View 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
View 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

Binary file not shown.

11
test_main.http Normal file
View 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
###