https://github.com/kiquetal/bons-reader-py
A Python-based web scraper that extracts the latest bond emissions from the AsunciΓ³n Stock Exchange (BVA). It automatically sends email alerts for high-yield bonds and supports local data ingestion for AI-powered semantic search.
https://github.com/kiquetal/bons-reader-py
beautifulsoup bonds financial-data github-actions paraguay python requests scraper
Last synced: 3 days ago
JSON representation
A Python-based web scraper that extracts the latest bond emissions from the AsunciΓ³n Stock Exchange (BVA). It automatically sends email alerts for high-yield bonds and supports local data ingestion for AI-powered semantic search.
- Host: GitHub
- URL: https://github.com/kiquetal/bons-reader-py
- Owner: kiquetal
- Created: 2026-04-21T17:00:19.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-04-21T23:03:58.000Z (about 2 months ago)
- Last Synced: 2026-04-22T00:35:23.194Z (about 2 months ago)
- Topics: beautifulsoup, bonds, financial-data, github-actions, paraguay, python, requests, scraper
- Language: Python
- Homepage:
- Size: 227 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Agents: AGENTS.md
Awesome Lists containing this project
README
# π BVA Emissions Scraper
Scrapes the latest bond emissions from [Bolsa de Valores de AsunciΓ³n](https://www.bolsadevalores.com.py/nuevas-emisiones/) (Paraguay), flattens multi-series emissions into individual entries, and sends email alerts when bonds exceed a configurable interest rate threshold.
## Architecture

The scraper fetches the listing page, handles AJAX pagination (JetEngine "Cargar mΓ‘s"), visits each detail page, and extracts per-series data into a flat JSON structure.
## Execution Environments

Runs locally with optional [MemPalace](https://github.com/MemPalace/mempalace) RAG ingestion, or on GitHub Actions every Tuesday and Thursday.
## Data Model

Each emission can contain multiple series. The scraper flattens them so every series is its own JSON entry with the parent emission's metadata.
### JSON Output Fields
| Field | Example | Description |
|---|---|---|
| `name` | BANCO FAMILIAR S.A.E.C.A. | Issuer name |
| `instrument` | bono | Instrument type |
| `qualification` | AApy Estable. | Risk rating |
| `percentage` | 5,65% | Interest rate |
| `date` | Jue, abril 23, 2026 | Emission date |
| `duration` | 732 | Term in days |
| `isin` | PYFAM03F0006 | ISIN code |
| `agente_colocador` | Familiar Casa de Bolsa S.A | Placement agent |
| `series_count` | 3 | Total series in the emission |
| `url` | https://...banco-familiar.../ | Detail page URL |
| `scraped_at` | 2026-04-21T16:00:13Z | UTC timestamp of scrape execution |
## Pre-requisites
- **Python 3.9+**
- **pip** (comes with Python)
- A [Resend](https://resend.com) account and API key (for email alerts)
- *(Optional)* [MemPalace](https://github.com/MemPalace/mempalace) installed locally for RAG ingestion
## Setup
```bash
# 1. Clone the repo
git clone && cd bons-reader-py
# 2. Create virtual environment and install dependencies
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# 3. Configure environment variables
cp .env.example .env
# Edit .env with your values:
# RESEND_API_KEY=re_xxxxxxxxxxxx
# EMAIL_FROM=onboarding@resend.dev
# EMAIL_TO=you@example.com
# MEMPALACE_BIN=/path/to/mempalace (optional, for --local mode)
```
## Usage
```bash
# Full run: scrape + save JSON + send email
.venv/bin/python scraper.py
# Scrape only, no email (for testing)
.venv/bin/python scraper.py --no-email
# Local run with MemPalace ingestion
.venv/bin/python scraper.py --local
```
Output is saved to `data/emisiones.json`.
## Email Alert
The email includes:
- **Summary** β total series count and how many exceed the interest threshold (default: 11%)
- **Highlighted bonds** β listed at the top with β οΈ warning
- **Full table** β all series with rows above threshold highlighted in yellow
Columns: Nombre, Instrumento, CalificaciΓ³n, Tasa, Fecha, Plazo, ISIN, Agente Colocador.
## Testing Email Locally
1. Create a free account at [resend.com](https://resend.com) and grab your API key.
2. Copy and edit the env file:
```bash
cp .env.example .env
```
3. Fill in your values:
```
RESEND_API_KEY=re_your_actual_key
EMAIL_FROM=onboarding@resend.dev
EMAIL_TO=your-email@example.com
```
> **Note:** If you haven't verified a custom domain in Resend, use `onboarding@resend.dev` as the sender. In that case, `EMAIL_TO` must match your Resend account email.
4. Run the scraper with email enabled:
```bash
.venv/bin/python scraper.py
```
5. To test scraping without sending email:
```bash
.venv/bin/python scraper.py --no-email
```
## MemPalace Integration
[MemPalace](https://github.com/MemPalace/mempalace) is a local-first AI memory system that stores content as verbatim text and retrieves it via semantic search. It organizes data into *wings* (projects), *rooms* (topics), and *drawers* (content) β all indexed locally with no API calls required.
When you run the scraper with `--local`, it automatically ingests the scraped emissions into MemPalace so you can later query them with natural language, e.g.:
```bash
# After running the scraper with --local
mempalace search "bonds with high interest rate"
```
The scraper calls `mempalace mine` automatically. If you want to manually ingest an updated `data/emisiones.json`, use:
```bash
./ingest-mempalace.sh
```
This script reads your local `MEMPALACE_BIN` from the `.env` file.
## GitHub Actions
The workflow runs every Tuesday and Thursday at 12:00 UTC (~8am Paraguay time) and can also be triggered manually.
### Required Secrets
Add these in your repo β Settings β Secrets and variables β Actions:
| Secret | Description |
|---|---|
| `RESEND_API_KEY` | Your Resend API key |
| `EMAIL_FROM` | Sender email (e.g. `alerts@yourdomain.com`) |
| `EMAIL_TO` | Recipient(s), comma-separated |
### Manual Trigger
Go to Actions β BVA Emissions Scraper β Run workflow.
## Project Structure
```
.
βββ scraper.py # Main script
βββ requirements.txt # Python dependencies
βββ .env.example # Environment variables template
βββ AGENTS.md # LLM/MemPalace context file
βββ .gitignore
βββ data/
β βββ .gitkeep
β βββ emisiones.json # Output (git-ignored)
βββ docs/
β βββ architecture.png
β βββ environments.png
β βββ data-model.png
βββ .github/workflows/bva-scraper.yml # GitHub Actions workflow
βββ PLAN.md # Implementation plan
βββ README.md
```
## How It Works
1. **Fetch listing page** β GET `/nuevas-emisiones/`, extract detail URLs from `data-url` attributes
2. **AJAX pagination** β POST to `admin-ajax.php` with JetEngine params and signature to load more entries beyond the initial 12
3. **Parse detail pages** β For each emission, extract name, instrument, qualification, date, and per-series: ISIN, interest rate, duration, placement agent
4. **Flatten** β Each series becomes its own JSON entry with parent emission metadata
5. **Save** β Write to `data/emisiones.json`
6. **Email** β Send via Resend with full table and highlighted rows for bonds > 11%
7. **MemPalace** *(local only)* β Ingest `data/` into mempalace for RAG search
## Financial Formulas
For academic reference, here are the formulas used to understand bond returns and interest accumulation.
### 1. Compound Interest
Used to calculate the total amount of an investment over time when interest is reinvested. In Paraguay, entities like **Cadiem** and **Investor** offer products like *Fondo Mutuo* that utilize compound interest by reinvesting daily yields.
$$A = P \left(1 + \frac{r}{n}\right)^{nt}$$
- **A**: Final amount (Principal + Interest)
- **P**: Principal (Initial investment)
- **r**: Annual interest rate (decimal)
- **n**: Number of times interest is compounded per year
- **t**: Number of years
### 2. Bond Interest (Coupon)
The periodic interest payment a bondholder receives from the bond's issuance date until it matures. Unlike compound interest, bond coupons are typically paid out (not reinvested automatically).
**Practical Example:**
- **Issuer:** TAPE RUVICHA S.A.E.C.A.
- **Principal:** 30,000,000 PYG (Approx. **$4,000 USD** at 7,500 PYG/USD)
- **Annual Rate:** 12.35%
$$C = \$4,000 \times 0.1235 = \$494 \text{ per year}$$
| Payment Period | Interest (USD) | Interest (PYG) |
| :--- | :--- | :--- |
| Quarterly | $123.50 | 926,250 PYG |
| Semi-Annual | $247.00 | 1,852,500 PYG |
| **Annual Total** | **$494.00** | **3,705,000 PYG** |
### π₯ The 5-Year Showdown: Compound vs. Bond
What happens if you hold both for 5 years with a **$4,000** initial capital?
| Investment Type | Annual Rate | Total Interest (5yr) | Final Value |
| :--- | :--- | :--- | :--- |
| **Fondo Mutuo** (Compound) | 9.00% | $2,154.48 | $6,154.48 |
| **Tape Ruvicha Bond** (Simple) | 12.35% | $2,470.00 | $6,470.00 |
| **Delta (Difference)** | **+3.35%** | **$315.52** | **Bond wins!** |
*Note: In the long run (15+ years), the power of compounding usually overtakes higher simple interest rates, but for shorter terms, the raw yield of high-rate bonds is often superior.*
$$C = F \times c$$
- **C**: Coupon payment amount
- **F**: Face value (Par value) of the bond
- **c**: Coupon rate (Annual interest rate)
### Visual Representation

| Year | Interest (9%) | Total Capital |
| :--- | :--- | :--- |
| 1 | $90.00 | $1,090.00 |
| 2 | $98.10 | $1,188.10 |
| 3 | $106.93 | $1,295.03 |
| 4 | $116.55 | $1,411.58 |
| 5 | $127.04 | $1,538.62 |
| 6 | $138.48 | $1,677.10 |
| 7 | $150.94 | $1,828.04 |
| 8 | $164.52 | $1,992.56 |
| 9 | $179.33 | $2,171.89 |
| 10 | $195.47 | $2,367.36 |
## License
Made in 2026