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/accountsGET /api/v1/holdingsfor each accountGET /api/v1/pricesfor held symbolsGET /api/v1/candlesfor one adjusted daily candle per held symbolGET /api/v1/exchange-ratefor 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.
uv sync --dev
uv run --env-file .env chatport collectOptional 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 every finished complete or partial capture that has not been
processed by the current parser version:
uv run --env-file .env chatport deriveThis 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.
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-onlyEach 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 275000Record 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 listFlows 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.
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 30For 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.
Configure optional Toss-supported symbols as explicitly named investable proxies:
CHATPORT_BENCHMARKS=sp500=SPY,nasdaq100=QQQ,gold=GLDThese 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 30Bitcoin, 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 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-bandIf --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.
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 webThe 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 8765The 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 rebuildThe 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 rebuildrebuild 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.
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 collectFor 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.gitdocs/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.
Create a consistent SQLite backup before upgrades or schema changes:
uv run --env-file .env chatport backup /path/to/backups/chatport.sqliteThe command refuses to overwrite an existing backup. Toss credentials are not
required when CHATPORT_DB_PATH is already present in the process environment.
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.
uv run pytest