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_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.
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 collectCreate 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