← Back to iOS & macOS Development
iOS & macOS Development by @mib424242

riskofficer

Manage investment portfolios, calculate risk metrics

New
Source Code

RiskOfficer Portfolio Management

This skill connects to RiskOfficer API to manage investment portfolios and calculate risks.

Setup

  1. Open RiskOfficer app → Settings → API Keys
  2. Create new token for "OpenClaw"
  3. Set environment variable: RISK_OFFICER_TOKEN=ro_pat_...

Or configure in ~/.openclaw/openclaw.json:

{
  "skills": {
    "entries": {
      "riskofficer": {
        "enabled": true,
        "apiKey": "ro_pat_..."
      }
    }
  }
}

API Base URL

https://api.riskofficer.tech/api/v1

All requests require header: Authorization: Bearer ${RISK_OFFICER_TOKEN}


Available Commands

Portfolio Management

List Portfolios

When user asks to see their portfolios, list portfolios, or show portfolio overview:

curl -s "https://api.riskofficer.tech/api/v1/portfolios/list" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}"

Response contains array of portfolios with: id, name, total_value, currency, positions_count, broker, sandbox.

Get Portfolio Details

When user asks about a specific portfolio or wants to see positions:

curl -s "https://api.riskofficer.tech/api/v1/portfolio/snapshot/{snapshot_id}" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}"

Response contains: name, total_value, currency, positions (array with ticker, quantity, current_price, value, weight).

Get Portfolio History

When user asks for portfolio history, how portfolio changed over time, or list of past snapshots:

curl -s "https://api.riskofficer.tech/api/v1/portfolio/history?days=30" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}"

Query params: days (optional, default 30, 1–365). Response: snapshots array with snapshot_id, timestamp, total_value, positions_count, sync_source, type (aggregated/manual/broker), name, broker, sandbox.

Get Snapshot Diff (compare two portfolio versions)

When user wants to compare two portfolio states (e.g. before/after rebalance, or two dates):

curl -s "https://api.riskofficer.tech/api/v1/portfolio/snapshot/{snapshot_id}/diff?compare_to={other_snapshot_id}" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}"

Response: added/removed/modified positions, total_value_delta. Both snapshots must belong to the user.

Get Aggregated Portfolio

When user asks for total/combined portfolio, overall position, or "show everything together":

curl -s "https://api.riskofficer.tech/api/v1/portfolio/aggregated?type=all" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}"

Query params:

  • type=production — manual + broker (sandbox=false)
  • type=sandbox — broker (sandbox=true) only
  • type=all — everything (default)

Response:

  • portfolio.positions — all positions merged across portfolios
  • portfolio.total_value — total value in base currency
  • portfolio.currency — base currency (RUB or USD)
  • portfolio.sources_count — number of portfolios aggregated

Example response:

{
  "portfolio": {
    "positions": [
      {"ticker": "SBER", "quantity": 150, "value": 42795, "sources": ["Т-Банк", "Manual"]},
      {"ticker": "AAPL", "quantity": 10, "value": 189500, "original_currency": "USD"}
    ],
    "total_value": 1500000,
    "currency": "RUB",
    "sources_count": 3
  },
  "snapshot_id": "uuid-of-aggregated"
}

Currency conversion: Positions in different currencies are automatically converted to base currency using current exchange rates (CBR for RUB).

Change Base Currency (Aggregated Portfolio)

When user wants to see aggregated portfolio in different currency:

curl -s -X PATCH "https://api.riskofficer.tech/api/v1/portfolio/{aggregated_snapshot_id}/settings" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"base_currency": "USD"}'

Supported currencies: RUB, USD

After changing, aggregated portfolio recalculates automatically.

User prompt examples:

  • "Покажи всё в долларах" → change base_currency to USD
  • "Переведи портфель в рубли" → change base_currency to RUB

Include/Exclude from Aggregated

When user wants to exclude a portfolio from total calculation:

curl -s -X PATCH "https://api.riskofficer.tech/api/v1/portfolio/{snapshot_id}/settings" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"include_in_aggregated": false}'

Use cases:

  • "Не учитывай песочницу в общем портфеле" → exclude sandbox
  • "Убери демо-портфель из расчёта" → exclude manual portfolio

Create Manual Portfolio

When user wants to create a new portfolio with specific positions:

curl -s -X POST "https://api.riskofficer.tech/api/v1/portfolio/manual" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Portfolio Name",
    "positions": [
      {"ticker": "SBER", "quantity": 100},
      {"ticker": "GAZP", "quantity": 50}
    ]
  }'

IMPORTANT RULE - Single Currency: All assets in a portfolio must be in the same currency.

  • RUB assets: SBER, GAZP, LKOH, YNDX, etc.
  • USD assets: AAPL, MSFT, GOOGL, etc. Cannot mix! If user tries to mix currencies, explain and suggest creating separate portfolios.

Update Portfolio (Add/Remove Positions)

When user wants to modify an existing portfolio:

  1. First get current portfolio to find the name:
curl -s "https://api.riskofficer.tech/api/v1/portfolio/snapshot/{snapshot_id}" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}"
  1. Then create new snapshot with updated positions (use same name):
curl -s -X POST "https://api.riskofficer.tech/api/v1/portfolio/manual" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "<same name from step 1>",
    "positions": [<updated list of all positions>]
  }'

IMPORTANT: Always show user what will change and ask for confirmation before updating.


Broker Integration

List Connected Brokers

When user asks about connected brokers or broker status:

curl -s "https://api.riskofficer.tech/api/v1/brokers/connections" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}"

Refresh Portfolio from Tinkoff

When user wants to sync/update portfolio from Tinkoff (broker must be connected via app):

curl -s -X POST "https://api.riskofficer.tech/api/v1/portfolio/proxy/broker/tinkoff/portfolio" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"sandbox": false}'

If response is 400 with missing_api_key, broker is not connected. Explain how to connect:

  1. Get API token from https://www.tbank.ru/invest/settings/api/
  2. Open RiskOfficer app → Settings → Brokers → Connect Tinkoff
  3. Paste token and connect

Risk Calculations

Calculate VaR (FREE)

When user asks to calculate risks, VaR, or risk metrics:

curl -s -X POST "https://api.riskofficer.tech/api/v1/risk/calculate-var" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "portfolio_snapshot_id": "{snapshot_id}",
    "method": "historical",
    "confidence": 0.95,
    "horizon_days": 1,
    "force_recalc": false
  }'
  • Methods: historical, parametric, garch
  • force_recalc (optional, default false): If user wants a fresh calculation ignoring cache (e.g. "recalculate VaR", "refresh risk"), set "force_recalc": true. Otherwise the API may return a cached result when prices have not changed.

This returns calculation_id. Poll for result:

curl -s "https://api.riskofficer.tech/api/v1/risk/calculation/{calculation_id}" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}"

Wait until status is done, then present results. If the POST response already has status: "done" and var_95/cvar_95 (cached result), you can present those without polling.

Get VaR / Risk Calculation History

When user asks for last risk calculations, previous VaR results, or "show my risk history":

curl -s "https://api.riskofficer.tech/api/v1/risk/history?limit=50" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}"

Query params: limit (optional, default 50, max 100).

Response: calculations array with calculation_id, portfolio_snapshot_id, status, method, var_95, cvar_95, sharpe_ratio, created_at, completed_at. Use to show a short list of recent VaR runs or to let user pick a past result.

Run Monte Carlo (QUANT - currently free for all users)

When user asks for Monte Carlo simulation:

curl -s -X POST "https://api.riskofficer.tech/api/v1/risk/monte-carlo" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "portfolio_snapshot_id": "{snapshot_id}",
    "simulations": 1000,
    "horizon_days": 365,
    "model": "gbm"
  }'

Poll: GET /api/v1/risk/monte-carlo/{simulation_id}

Run Stress Test (QUANT - currently free for all users)

When user asks for stress test:

First, get available crises:

curl -s "https://api.riskofficer.tech/api/v1/risk/stress-test/crises" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}"

Then run stress test:

curl -s -X POST "https://api.riskofficer.tech/api/v1/risk/stress-test" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "portfolio_snapshot_id": "{snapshot_id}",
    "crisis": "covid_19"
  }'

Poll: GET /api/v1/risk/stress-test/{stress_test_id}


Portfolio Optimization (QUANT - currently free for all users)

Risk Parity Optimization

When user asks to optimize portfolio or balance risks:

curl -s -X POST "https://api.riskofficer.tech/api/v1/portfolio/{snapshot_id}/optimize" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "optimization_mode": "preserve_directions",
    "constraints": {
      "max_weight": 0.30,
      "min_weight": 0.02
    }
  }'

Modes:

  • long_only: All weights ≥ 0
  • preserve_directions: Keep long/short as-is
  • unconstrained: Any direction allowed

Poll: GET /api/v1/portfolio/optimizations/{optimization_id} Result: GET /api/v1/portfolio/optimizations/{optimization_id}/result

Calmar Ratio Optimization

When user asks for Calmar optimization, maximize Calmar Ratio (CAGR / |Max Drawdown|). Requires 200+ trading days of price history per ticker (backend requests 252 days). If user has short history, suggest Risk Parity instead.

curl -s -X POST "https://api.riskofficer.tech/api/v1/portfolio/{snapshot_id}/optimize-calmar" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "optimization_mode": "long_only",
    "constraints": {
      "max_weight": 0.50,
      "min_weight": 0.05,
      "min_expected_return": 0.0,
      "max_drawdown_limit": 0.15,
      "min_calmar_target": 0.5
    }
  }'

Poll: GET /api/v1/portfolio/optimizations/{optimization_id} (check optimization_type === "calmar_ratio").
Result: GET /api/v1/portfolio/optimizations/{optimization_id}/result — includes current_metrics, optimized_metrics (cagr, max_drawdown, calmar_ratio, recovery_time_days).
Apply: same as Risk Parity — POST /api/v1/portfolio/optimizations/{optimization_id}/apply.

Apply Optimization

IMPORTANT: Always show rebalancing plan and ask for explicit user confirmation first!

curl -s -X POST "https://api.riskofficer.tech/api/v1/portfolio/optimizations/{optimization_id}/apply" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}"

Subscription Status

Note: Quant subscription is currently FREE for all users. All features work without payment.

Check Subscription

When you need to check if user has Quant subscription:

curl -s "https://api.riskofficer.tech/api/v1/subscription/status" \
  -H "Authorization: Bearer ${RISK_OFFICER_TOKEN}"

Currently all users have has_subscription: true (free tier enabled).


Async Operations

VaR, Monte Carlo, Stress Test, and Optimization are asynchronous.

Polling pattern:

  1. Call POST endpoint → get calculation_id / simulation_id / optimization_id
  2. Poll GET endpoint every 2-3 seconds
  3. Check status field:
    • pending or processing → keep polling
    • done → present results
    • failed → show error message

Typical times:

Operation Typical Time
VaR 3-10 seconds
Monte Carlo 10-30 seconds
Stress Test 5-15 seconds
Optimization 10-30 seconds

User communication:

  • Show "Calculating..." message immediately after starting
  • If polling takes >10 seconds, update: "Still calculating... please wait"
  • Always show result or error when complete

Important Rules

  1. Single Currency Rule (Manual/Broker portfolios): Each individual portfolio must have same-currency assets. Cannot mix SBER (RUB) with AAPL (USD) in one manual portfolio. Suggest separate portfolios.

  2. Aggregated Portfolio: The aggregated portfolio CAN contain assets in different currencies - they are automatically converted to base currency (RUB or USD) using CBR rates.

  3. Subscription: Monte Carlo, Stress Test, and Optimization are Quant features (currently free for all users). VaR is always FREE.

  4. Broker Integration: User must connect broker in RiskOfficer app first. Cannot connect via chat (security).

  5. Confirmations: Before applying optimizations or making significant portfolio changes, always show what will change and ask for confirmation.

  6. Async Operations: VaR, Monte Carlo, Stress Test, and Optimization are async. Poll for results.

  7. Error Handling:

    • 401 Unauthorized → Token invalid or expired, user needs to recreate
    • 403 subscription_required → Need Quant subscription (currently free for all)
    • 400 missing_api_key → Broker not connected
    • 400 currency_mismatch → Mixed currencies

Example Conversations

User wants portfolio overview

User: "Show my portfolios" → Call GET /portfolios/list → Format nicely with values, positions count, last updated

User wants combined/total portfolio

User: "Покажи всё вместе" / "Total portfolio" / "Сколько у меня всего?" → Call GET /portfolio/aggregated?type=all → Show total value, all positions merged, sources count → Note which positions were converted from other currencies

User wants to change display currency

User: "Покажи в долларах" / "Switch to USD" → Call PATCH /portfolio/{aggregated_id}/settings with {"base_currency": "USD"} → Call GET /portfolio/aggregated again → Show portfolio in new currency

User wants to analyze risks

User: "What are the risks of my main portfolio?" → Call GET /portfolios/list to find the portfolio → Call POST /risk/calculate-var → Poll until done → Present VaR, CVaR, volatility, risk contributions → Offer optimization if risks are unbalanced

User wants Calmar optimization

User: "Оптимизируй портфель по Калмару" / "Optimize using Calmar Ratio" / "Maximize return per drawdown" → Call GET /portfolios/list or aggregated to get snapshot_id → Call POST /portfolio/{snapshot_id}/optimize-calmar with optimization_mode and optional constraints → If 400 INSUFFICIENT_HISTORY: explain need 200+ trading days of history, suggest Risk Parity as alternative → Poll GET /optimizations/{id} until status is done → Call GET /optimizations/{id}/result — show current_metrics vs optimized_metrics (Calmar ratio, CAGR, max drawdown) → Show rebalancing plan and ask for confirmation before apply

User tries to mix currencies

User: "Add Apple to my portfolio" → Check portfolio currency (RUB) vs AAPL currency (USD) → Explain cannot mix, suggest creating separate USD portfolio

User requests Monte Carlo or Stress Test

User: "Run Monte Carlo" → Call POST /risk/monte-carlo with portfolio snapshot → Poll until done → Present simulation results with percentiles and projections

User asks for risk or VaR history

User: "Show my last VaR results" / "Previous risk calculations" / "История расчётов рисков" → Call GET /risk/history?limit=50 → Present list of recent calculations (method, var_95, cvar_95, date)

User asks for portfolio history

User: "How did my portfolio change?" / "История портфеля" → Call GET /portfolio/history?days=30 → Present snapshots (date, total_value, positions_count, source)

User wants to compare two portfolio versions

User: "Compare my portfolio now vs last week" / "Что изменилось в портфеле?" → Get two snapshot_ids from GET /portfolio/history (or from context) → Call GET /portfolio/snapshot/{snapshot_id}/diff?compare_to={other_snapshot_id} → Present added/removed/modified positions and value delta