Skip to content
meteorohPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

13 Commits

Folders and files

Repository files navigation

Chatport

Chatport is a small, self-hosted collector for Toss Invest portfolio snapshots. It stores every data API response in SQLite as a BLOB and commits it before any JSON parsing or portfolio logic runs.

Raw capture and normalization are separate commands. A parser or analytics failure cannot undo or block an already committed capture.

Collected endpoints:

  • GET /api/v1/accounts
  • GET /api/v1/holdings for each account
  • GET /api/v1/prices for held symbols
  • GET /api/v1/candles for one adjusted daily candle per held symbol
  • GET /api/v1/exchange-rate for USD/KRW

Successful OAuth token response bodies are intentionally never retained. Non-success OAuth error bodies are retained for diagnosis because they do not contain an issued access token.

Setup

uv sync --dev
uv run --env-file .env chatport collect

Optional environment variables:

Variable Default
CHATPORT_DB_PATH data/chatport.sqlite
CHATPORT_LOCK_PATH database path with .lock suffix
CHATPORT_LOG_PATH logs/chatport.jsonl
CHATPORT_LOG_LEVEL INFO
CHATPORT_MAX_ATTEMPTS 3
CHATPORT_BENCHMARKS empty

Run records and API attempts use UTC timestamps. Exit code 0 means complete, 2 means partial, and 1 means failed or invalid configuration.

Normalize captured data

Normalize every finished complete or partial capture that has not been processed by the current parser version:

uv run --env-file .env chatport derive

This command does not call Toss and does not require TOSS_CLIENT_ID or TOSS_CLIENT_SECRET. It reads the immutable api_attempts rows and writes rebuildable account, position, current-price, adjusted daily-close, FX, and provisional valuation snapshots. Decimal values remain SQLite TEXT after validation so calculations use Python Decimal without a binary floating-point conversion.

The provisional valuation is explicitly securities_only: cash, receivables, liabilities, dividends, fees, taxes, and external cash flows are excluded. USD positions use the captured USD/KRW midRate. total_value_krw is NULL unless the source capture is complete and every position has a supported, sufficiently fresh close and any required FX observation. known_value_krw exists only for diagnosis and must not be treated as total portfolio value.

Manual cash and external flows

Toss exposes cash-based buying power, not an authoritative cash-balance snapshot, so Chatport does not silently treat buying power as portfolio cash. Record an explicit KRW and USD balance for every account, including zero balances:

uv run --env-file .env chatport cash set \
  --account 1 --currency KRW --amount 250000 \
  --at 2026-07-10T07:00:00+09:00

uv run --env-file .env chatport cash set \
  --account 1 --currency USD --amount 0 \
  --at 2026-07-10T07:00:00+09:00

uv run --env-file .env chatport cash list --active-only

Each cash set appends a new as-of observation. Correct a mistaken entry without editing history:

uv run --env-file .env chatport cash correct ENTRY_ID --amount 275000

Record deposits and withdrawals separately from balance observations:

uv run --env-file .env chatport flow add \
  --account 1 --currency KRW --type deposit --amount 1000000 \
  --at 2026-07-10T12:00:00+09:00

uv run --env-file .env chatport flow reverse FLOW_ID --note "duplicate entry"
uv run --env-file .env chatport flow list

Flows are append-only accounting evidence. They do not change NAV; they adjust the return calculation so deposits and withdrawals are not mistaken for investment performance.

Derivation selects the latest active balance whose effective timestamp is at or before each valuation. portfolio_nav_snapshots.total_nav_krw is populated only when the securities valuation is complete and every account has both required cash balances. After adding or correcting historical cash, run chatport rebuild, or force one affected capture with chatport derive --run-id RUN_ID --force.

Unitized performance

Every derive run rebuilds the performance series from complete NAV snapshots and external flows. It uses Modified Dietz time weights based on the exact flow timestamps, then links period returns through a unit-price index initialized at KRW 1,000:

period return = (ending NAV - beginning NAV - net flow)
                / (beginning NAV + time-weighted flow)

Inspect or explicitly recalculate the series without Toss credentials:

uv run --env-file .env chatport performance rebuild
uv run --env-file .env chatport performance list --limit 30

For USD flows, the conversion uses the ending valuation's captured USD/KRW midRate and records its source attempt. This is a documented approximation because Chatport does not have an FX observation at every intraday flow. Performance is calculated only from complete NAV snapshots. An invalid period, such as a nonpositive Modified Dietz denominator, blocks subsequent periods until the inputs are corrected and the series is rebuilt.

This is an approximate linked time-weighted return, not a claim of GIPS compliance or a true intraday TWR. Returns shorter than one year are not annualized.

Benchmark proxies

Configure optional Toss-supported symbols as explicitly named investable proxies:

CHATPORT_BENCHMARKS=sp500=SPY,nasdaq100=QQQ,gold=GLD

These examples are ETF proxies, not the official S&P 500, Nasdaq-100, or spot gold indices. Fund expenses, tracking differences, taxes, distributions, and the exact meaning of Toss's adjusted candles can make their returns differ from official total-return indices. No benchmark is enabled by default.

For each configured proxy, collect stores the latest 30 adjusted daily candles. Benchmark failures are nonblocking: they are captured and logged but cannot make an otherwise complete portfolio run partial. derive calculates each daily proxy return from adjacent candles in the same response, allowing a later response to restate adjusted-price history without creating a false split or distribution jump. A gap beyond the rolling recovery window invalidates that proxy series rather than inventing a return.

Each proxy index starts at 1,000. Comparisons are rebased at the first portfolio-performance observation that has a same-run benchmark capture and include portfolio return, proxy return, arithmetic excess return, and relative wealth return. The proxy index and local-currency return are retained as captured. For the actual comparison, USD proxy wealth is converted to the KRW reporting currency with each run's USD/KRW midRate; both the current and baseline rates and their raw source-attempt IDs are stored. A USD proxy capture without same-run FX evidence still contributes to its local index, but its portfolio comparison is omitted and a warning is logged rather than silently mixing currencies.

Schema migration 7 removes only legacy mixed-currency comparison rows. After upgrading an existing database, run chatport benchmark rebuild once to recreate them with the stored same-run FX observations.

uv run --env-file .env chatport benchmark rebuild
uv run --env-file .env chatport benchmark list --limit 30
uv run --env-file .env chatport benchmark list --key sp500 --limit 30

Bitcoin, CPI, housing prices, and deposit rates are intentionally not forced through this daily Toss-symbol pipeline. They require separate sources, frequencies, licensing checks, and alignment policies.

Target allocation and rebalancing

Target policies are append-only, effective-dated inputs. Percentages are entered as percentage points, and the cash target may be explicit or inferred as the remainder to 100%:

uv run --env-file .env chatport allocation set \
  --target AAPL=40 --target SPY=35 --target GLD=15 \
  --cash-target 10 --threshold 5 \
  --at 2026-07-11T07:00:00+09:00 \
  --note "initial target policy"

uv run --env-file .env chatport allocation policies
uv run --env-file .env chatport allocation rebuild
uv run --env-file .env chatport allocation list --out-of-band

If --cash-target is omitted, Chatport assigns the unused percentage to cash. If it is provided, security and cash targets must total exactly 100%. The threshold is an absolute percentage-point drift; a row is out of band when its absolute drift is greater than or equal to the threshold.

Calculations use only complete KRW NAV snapshots. Current security values are aggregated across accounts by symbol, combined KRW and converted USD cash is a single CASH allocation, and any held symbol absent from the policy has a 0% target. Suggested rebalance_amount_krw is positive for a buy and negative for a sell. The cash row uses raise_cash or deploy_cash. Historical NAVs before the first policy's effective time are not backfilled.

These are value-level planning amounts, not executable orders. They do not account for whole-share constraints, spreads, fees, taxes, settlement, or minimum cash buffers beyond the configured cash target. Chatport never places orders. Every daily derive refreshes these snapshots; allocation rebuild is for immediate recalculation after adding or correcting a historical policy.

Authenticated web interface

The web process serves the read-only portfolio overview and a narrow append-only accounting interface. Configure a single local password and a separate high-entropy session-signing secret:

CHATPORT_WEB_PASSWORD=replace-with-a-long-local-password
CHATPORT_WEB_SESSION_SECRET=replace-with-at-least-32-random-characters
Variable Web behavior
CHATPORT_WEB_PASSWORD Required; at least 10 characters
CHATPORT_WEB_SESSION_SECRET Required; at least 32 characters; signs sessions
CHATPORT_WEB_HTTPS_ONLY Optional; false by default, set true behind HTTPS
CHATPORT_LOCK_PATH Optional; shared lock used by accounting and other commands

Generate a session secret locally, for example:

uv run python -c "import secrets; print(secrets.token_urlsafe(48))"

Both variables are required; the web command refuses to start when either is missing or too short. Then run the server as its own process:

uv run --env-file .env chatport web

The web command binds to 127.0.0.1:8000 by default and always starts one Uvicorn worker. Override the port or database explicitly when needed:

uv run --env-file .env chatport web \
  --database /srv/chatport/data/chatport.sqlite \
  --port 8765

The web process does not require Toss credentials. All overview and accounting history queries open the existing SQLite database with mode=ro and PRAGMA query_only = ON. A missing or schema-incompatible database leaves the views unavailable and makes unauthenticated GET /health return 503 without creating or upgrading the file. Back up and migrate through the existing non-web operational workflow before starting the web process after a schema-changing release.

Authentication uses a signed, HTTP-only, same-site session cookie and every POST form requires a session-bound CSRF token. Set CHATPORT_WEB_HTTPS_ONLY=true when the browser reaches Chatport through HTTPS; leave it false for direct localhost HTTP. The default bind remains 127.0.0.1; reverse-proxy authentication and public-network exposure remain separate deployment concerns.

The only web database-write path appends records through AccountingLedger to cash_balance_entries or external_cash_flows. It opens only an existing database with SQLite mode=rw, verifies the exact schema version, takes CHATPORT_LOCK_PATH, starts one short immediate transaction, and installs an SQLite authorizer that denies updates, deletes, schema changes, and writes to every other table. Cash corrections insert a superseding row and flow reversals insert an opposite row; prior evidence is never modified or deleted. The web process never runs collection, derivation, performance or allocation rebuilds, migrations, or orders.

Use the Accounting page to record KRW/USD balances, deposits, withdrawals, corrections, and reversals and to inspect their active, superseded, or reversed status. Historical accounting changes can make stored NAV, performance, benchmark, or allocation snapshots stale. The page only explains the needed CLI follow-up; it never triggers one automatically. After reviewing the change and taking a backup when appropriate, run:

uv run --env-file .env chatport rebuild

The overview displays only the latest complete total_nav_krw. It never uses known_nav_krw or another incomplete aggregate as a fallback. Newer partial capture, derivation, or NAV-input states remain visible in the health section so an older complete value is not mistaken for current complete data. Web queries do not select raw API response bodies, request metadata, credentials, tokens, or brokerage account identifiers.

HTMX 2.0.10 and Chart.js 4.5.1 are stored under chatport/web/static and served locally. The overview loads HTMX for its refresh partial; Chart.js is vendored for later chart pages but is not loaded by the chartless overview or accounting page.

Useful recovery commands:

# Reprocess one run with the current parser.
uv run --env-file .env chatport derive --run-id RUN_ID --force

# Delete normalized rows and deterministically recreate them from raw captures.
uv run --env-file .env chatport rebuild

rebuild never deletes or updates runs or api_attempts. Run a backup first anyway when using it operationally. A run captured by an older Chatport version without /api/v1/candles remains incomplete after rebuilding because derivation never invents or retroactively fetches missing raw evidence.

Cron

Use the same environment as the interactive command. Chatport also takes its own nonblocking lock, so a second invocation exits rather than overlapping an active run.

0 7 * * * cd /path/to/chatport && /path/to/uv run --no-sync --env-file /path/to/chatport.env chatport collect

Debian LXC installation

For a fresh Debian systemd host, including a Proxmox LXC, use the checked-in installer from a reviewed checkout:

sudo ./deploy/install.sh --repo https://github.com/meteoroh/chatport.git

docs/lxc-install.md is the full runbook: the order to run things in, the first capture and the cash balances it needs, upgrading, and what to check when the overview stays empty.

It installs uv, a dedicated chatport service account, locked production dependencies, and weekday systemd timers for collection at 07:00 KST, derivation at 07:10 KST, and a backup at 07:30 KST that keeps the 30 most recent copies. It never overwrites an existing SQLite database or environment file, and it refuses to update while a collection or derivation is running.

The database lives in /var/lib/chatport, logs in /var/log/chatport, and secrets in /etc/chatport/chatport.env, which is readable but not writable by the service account. Fill the Toss credentials there, run one manual collection and derivation, record the cash balances described under Manual cash and external flows, then enable the timers. A different release tag or schedule can be selected with --ref, --collect-time, --derive-time, and --backup-time.

The web process described under Authenticated web interface is installed and started on 127.0.0.1:8000, with CHATPORT_WEB_PASSWORD and CHATPORT_WEB_SESSION_SECRET generated into the environment file on first install. Pass --no-web to install the CLI timers alone. Pass --no-sandbox when a restrictive LXC host rejects the ProtectSystem=strict unit hardening. Run ./deploy/install.sh --help for the full option list.

--web-host overrides the bind address. Any value other than 127.0.0.1 publishes the login page to that network over plain HTTP, and Chatport has no login rate limiting and no authentication logging, so restrict which source addresses can reach the port.

CHATPORT_LOCK_PATH must be a separate file from CHATPORT_DB_PATH.

Backup

Create a consistent SQLite backup before upgrades or schema changes:

uv run --env-file .env chatport backup /path/to/backups/chatport.sqlite

The command refuses to overwrite an existing backup. Toss credentials are not required when CHATPORT_DB_PATH is already present in the process environment.

Production sequence

Keep collection as the primary independent job. On the NAS, run normalization as a second job shortly afterward:

07:00  chatport collect
07:10  chatport derive

If the second process is interrupted, the raw response remains available and the next derive invocation retries the unfinished run. A response rejected by the strict parser remains recorded as a failed derivation for diagnosis; after fixing the parser, use --force for one run or rebuild for the full history. After installing a release that changes the parser version, back up the database and run chatport rebuild once before enabling analytics based on the normalized tables.

The initial valuation assumptions for future analytics are documented in docs/valuation-policy.md.

Tests

uv run pytest

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages