record-trade
Record Trade
Record executed trades to the Spot Canvas trading ledger via its REST import API.
Endpoint: POST /api/v1/import
The ledger URL depends on the environment:
- Staging:
https://signalngn-trader-staging.europe-west1.run.app - Production: Use the
LEDGER_URLenvironment variable if set, otherwise ask the user.
Recording a Trade
Use curl to POST a JSON body with one or more trades.
curl -s -X POST "${TRADER_URL:-https://signalngn-trader-staging.europe-west1.run.app}/api/v1/import" \
-H "Content-Type: application/json" \
-d '{
"trades": [{
"trade_id": "<unique-id>",
"account_id": "<account>",
"symbol": "<pair>",
"side": "<buy|sell>",
"quantity": <number>,
"price": <number>,
"fee": <number>,
"fee_currency": "<currency>",
"market_type": "<spot|futures>",
"timestamp": "<RFC3339>",
"strategy": "<strategy-name>",
"entry_reason": "<signal description>",
"exit_reason": "<exit description>",
"confidence": <0-1>,
"stop_loss": <price>,
"take_profit": <price>,
"leverage": <integer>,
"margin": <number>,
"liquidation_price": <number>,
"funding_fee": <number>
}]
}'
Field Reference
Required Fields
| Field | Type | Description | Example |
|---|---|---|---|
trade_id |
string | Unique trade identifier. Use exchange trade ID or generate a UUID. Must be unique — duplicates are silently skipped. | "binance-12345" |
account_id |
string | Account identifier. Accounts are auto-created on first use. Use "live" or "paper" convention. |
"live" |
symbol |
string | Trading pair. | "BTC-USD" |
side |
string | "buy" or "sell" |
"buy" |
quantity |
number | Trade quantity (must be > 0). | 0.5 |
price |
number | Execution price (must be > 0). | 50000 |
fee |
number | Fee amount. Use 0 if no fee. |
25.00 |
fee_currency |
string | Currency the fee is denominated in. | "USD" |
market_type |
string | "spot" or "futures" |
"spot" |
timestamp |
string | Trade execution time in RFC3339 format. | "2025-06-15T10:30:00Z" |
Strategy Metadata (optional)
| Field | Type | Description | Example |
|---|---|---|---|
strategy |
string | Strategy name that generated the signal. | "macd-rsi-v2" |
entry_reason |
string | Signal reason text at entry. Use on buy trades. | "MACD bullish crossover, RSI 42" |
exit_reason |
string | Reason for closing. Use on sell trades. | "stop loss hit", "take profit reached" |
confidence |
number | Signal confidence score, 0–1. | 0.85 |
stop_loss |
number | Stop loss price level. | 48000 |
take_profit |
number | Take profit price level. | 55000 |
Futures Fields (optional, for market_type: "futures" only)
| Field | Type | Description | Example |
|---|---|---|---|
leverage |
integer | Leverage multiplier. | 10 |
margin |
number | Margin amount. | 5000 |
liquidation_price |
number | Liquidation price. | 45000 |
funding_fee |
number | Funding fee amount. | 12.50 |
Response Format
{
"total": 1,
"inserted": 1,
"duplicates": 0,
"errors": 0,
"results": [
{ "trade_id": "binance-12345", "status": "inserted" }
]
}
Status per trade is one of: "inserted", "duplicate", "error".
Examples
Spot buy with strategy metadata
curl -s -X POST "${TRADER_URL:-https://signalngn-trader-staging.europe-west1.run.app}/api/v1/import" \
-H "Content-Type: application/json" \
-d '{
"trades": [{
"trade_id": "bot-'$(date +%s)'",
"account_id": "live",
"symbol": "BTC-USD",
"side": "buy",
"quantity": 0.5,
"price": 50000,
"fee": 25,
"fee_currency": "USD",
"market_type": "spot",
"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'",
"strategy": "macd-rsi-v2",
"entry_reason": "MACD bullish crossover, RSI 42",
"confidence": 0.85,
"stop_loss": 48000,
"take_profit": 55000
}]
}'
Spot sell (closing position)
curl -s -X POST "${TRADER_URL:-https://signalngn-trader-staging.europe-west1.run.app}/api/v1/import" \
-H "Content-Type: application/json" \
-d '{
"trades": [{
"trade_id": "bot-'$(date +%s)'",
"account_id": "live",
"symbol": "BTC-USD",
"side": "sell",
"quantity": 0.5,
"price": 55000,
"fee": 27.50,
"fee_currency": "USD",
"market_type": "spot",
"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'",
"exit_reason": "take profit reached"
}]
}'
Leveraged futures trade
curl -s -X POST "${TRADER_URL:-https://signalngn-trader-staging.europe-west1.run.app}/api/v1/import" \
-H "Content-Type: application/json" \
-d '{
"trades": [{
"trade_id": "bot-futures-'$(date +%s)'",
"account_id": "live",
"symbol": "ETH-USD",
"side": "buy",
"quantity": 10,
"price": 3000,
"fee": 6,
"fee_currency": "USD",
"market_type": "futures",
"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'",
"strategy": "funding-arb",
"confidence": 0.72,
"leverage": 5,
"margin": 6000,
"liquidation_price": 2500,
"stop_loss": 2800,
"take_profit": 3300
}]
}'
Batch import (multiple trades)
Up to 1000 trades per request. Trades are automatically sorted by timestamp for correct position calculation.
curl -s -X POST "${TRADER_URL:-https://signalngn-trader-staging.europe-west1.run.app}/api/v1/import" \
-H "Content-Type: application/json" \
-d '{
"trades": [
{ "trade_id": "t1", "account_id": "paper", "symbol": "BTC-USD", "side": "buy", "quantity": 1.0, "price": 40000, "fee": 20, "fee_currency": "USD", "market_type": "spot", "timestamp": "2025-06-01T10:00:00Z" },
{ "trade_id": "t2", "account_id": "paper", "symbol": "BTC-USD", "side": "sell", "quantity": 1.0, "price": 42000, "fee": 21, "fee_currency": "USD", "market_type": "spot", "timestamp": "2025-06-15T10:00:00Z", "exit_reason": "target hit" }
]
}'
Account Balance
The ledger tracks a cash balance per account. The trading agent should set an initial balance before trading and query it before sizing positions. Balance is automatically adjusted by trade ingestion — opening a position deducts the cost and closing a position credits the realised P&L.
Set (or reset) account balance
Use PUT /api/v1/accounts/{accountId}/balance to set an initial balance or to manually correct it after broker reconciliation. This overwrites the current value unconditionally.
curl -s -X PUT "${TRADER_URL:-https://signalngn-trader-staging.europe-west1.run.app}/api/v1/accounts/live/balance" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${LEDGER_API_KEY}" \
-d '{"amount": 50000, "currency": "USD"}'
Response:
{ "account_id": "live", "currency": "USD", "amount": 50000 }
currency defaults to "USD" if omitted.
CLI equivalent:
trader accounts balance set live 50000 # set USD balance
trader accounts balance set live 40000 --currency EUR # set EUR balance
trader accounts balance set live 50000 --json # raw JSON response
Query account balance
curl -s "${TRADER_URL:-https://signalngn-trader-staging.europe-west1.run.app}/api/v1/accounts/live/balance" \
-H "Authorization: Bearer ${LEDGER_API_KEY}"
Response when set:
{ "account_id": "live", "currency": "USD", "amount": 47250.00 }
Returns HTTP 404 when no balance has been set for the account.
Optional ?currency=EUR query parameter selects a non-default currency.
CLI equivalent:
trader accounts balance get live # show balance table
trader accounts balance get live --currency EUR # EUR balance
trader accounts balance get live --json # raw JSON
How automatic balance adjustment works
When a trade is ingested the ledger adjusts the USD balance within the same transaction as the position update. The adjustment is a no-op if no balance row exists for the account — no balance row is ever auto-created.
| Event | Balance change |
|---|---|
| Spot buy (open / add to position) | − quantity × price + fee |
| Spot sell (partial or full close) | + realised P&L |
| Futures open | − margin (uses margin field; falls back to cost_basis / leverage; skipped if neither available) |
| Futures close (partial or full) | + realised P&L (leverage- and fee-adjusted) |
Position rebuild does not touch the balance — it only reconstructs position state.
Balance in portfolio and stats responses
When a balance has been set, it is included in the portfolio summary and account stats responses as an optional "balance" field. It is omitted entirely when no balance row exists.
curl -s "${TRADER_URL:-...}/api/v1/accounts/live/portfolio" | python3 -m json.tool
# → { "positions": [...], "total_realized_pnl": 1234.5, "balance": 47250.00 }
curl -s "${TRADER_URL:-...}/api/v1/accounts/live/stats" | python3 -m json.tool
# → { ..., "total_realized_pnl": 1234.5, "balance": 47250.00 }
Querying the Ledger
After recording trades, you can query positions and trade history.
Get account stats (win rate, realized P&L, trade count)
REST endpoint — returns aggregate stats computed from round-trips (closed positions):
curl -s "${TRADER_URL:-https://signalngn-trader-staging.europe-west1.run.app}/api/v1/accounts/live/stats" | python3 -m json.tool
Response:
{
"total_trades": 116,
"closed_trades": 109,
"win_count": 79,
"loss_count": 30,
"win_rate": 0.7248,
"total_realized_pnl": 2555.28,
"open_positions": 7,
"balance": 47250.00
}
total_trades = closed + open positions (round-trips, not raw buy/sell records). balance is omitted when no balance has been set.
CLI equivalent:
trader accounts show live # human-readable table (includes balance row when set)
trader accounts show live --json # raw JSON
Check open positions
curl -s "${TRADER_URL:-https://signalngn-trader-staging.europe-west1.run.app}/api/v1/accounts/live/positions?status=open" | python3 -m json.tool
Check closed positions (with exit_price, exit_reason)
curl -s "${TRADER_URL:-https://signalngn-trader-staging.europe-west1.run.app}/api/v1/accounts/live/positions?status=closed" | python3 -m json.tool
Get portfolio summary
curl -s "${TRADER_URL:-https://signalngn-trader-staging.europe-west1.run.app}/api/v1/accounts/live/portfolio" | python3 -m json.tool
List recent trades
curl -s "${TRADER_URL:-https://signalngn-trader-staging.europe-west1.run.app}/api/v1/accounts/live/trades?limit=10" | python3 -m json.tool
Filter trades by symbol
curl -s "${TRADER_URL:-https://signalngn-trader-staging.europe-west1.run.app}/api/v1/accounts/live/trades?symbol=BTC-USD&limit=20" | python3 -m json.tool
trader CLI Reference
The trader CLI wraps the REST API for human use. Key commands:
trader accounts list # list all accounts for the tenant
trader accounts show <account-id> # show aggregate stats (win rate, P&L, trade count, balance)
trader accounts show <account-id> --json # raw JSON stats
trader accounts balance set <account-id> <amount> # set/overwrite account balance (USD)
trader accounts balance set <account-id> <amount> --currency EUR # set non-USD balance
trader accounts balance get <account-id> # query current balance
trader accounts balance get <account-id> --currency EUR # query non-USD balance
trader trades list <account-id> # round-trip view: one row per position (default)
trader trades list <account-id> --raw # raw trade view: one row per individual trade leg
trader trades list <account-id> --limit 20 # show last 20 positions (default: 50, 0 = all)
trader trades list <account-id> --long # show all columns: ID, full timestamps, exit reason
# default (--short): no ID, compact times (no year)
Round-trip view (default)
Shows one row per position. Columns: RESULT, SYMBOL, DIR, SIZE, ENTRY, EXIT, P&L, P&L%, OPENED, CLOSED.
RESULT:✓ win,✗ loss, oropenSIZE: cost basis in USDENTRY/EXIT: avg entry price and exit priceP&L/P&L%: realized P&L amount and percentageOPENED/CLOSED: compact timestamps (MM-DD HH:MM:SS);--longshows fullYYYY-MM-DD HH:MM:SS
A win/loss summary is printed below the table, calculated from the displayed rows:
5 wins 2 losses 71% win rate (7 closed)
Open positions are excluded from the win/loss count.
Raw trade view (--raw)
Shows one row per individual trade leg. Columns (short): SYMBOL, SIDE, QTY, PRICE, FEE, MARKET, TIME.
With --long: adds the TRADE-ID column and shows full timestamps.
Number formatting:
- QTY: whole numbers for large quantities (e.g.
52447552not5.24e+07) - PRICE: up to 6 decimal places for normal prices; scientific notation (
2.860e-05) for micro-prices - FEE: max 4 decimal places, trailing zeros trimmed
Deleting a Trade
Use this only to remove test trades that were recorded by mistake or during testing. Do not use it to delete real trading history.
A trade can only be deleted if its account/symbol has no open position. If the trade contributed to an open position, close the position first (record the offsetting sell/buy trade), then delete the test trades.
Via CLI
trader trades delete <trade-id> --confirm
The --confirm flag is required to prevent accidental deletion.
Examples:
# Delete a specific test trade
trader trades delete bot-1234567890 --confirm
# Delete and get JSON response
trader trades delete bot-1234567890 --confirm --json
Exit codes and messages:
| Situation | Output | Exit code |
|---|---|---|
| Success | deleted trade <id> |
0 |
Missing --confirm |
use --confirm to delete a trade |
non-zero |
| Trade not found | trade not found |
non-zero |
| Trade has open position | server error message | non-zero |
Via REST API
curl -s -X DELETE \
"${TRADER_URL:-https://signalngn-trader-staging.europe-west1.run.app}/api/v1/trades/<trade-id>" \
-H "Authorization: Bearer ${LEDGER_API_KEY}"
Responses:
| Status | Meaning |
|---|---|
200 {"deleted": "<id>"} |
Trade deleted successfully |
404 {"error": "trade not found"} |
Trade doesn't exist or belongs to another tenant |
409 {"error": "trade contributes to an open position and cannot be deleted"} |
Close the position first |
401 |
Invalid or missing API key |
Important Notes
- Idempotent: Submitting the same
trade_idtwice results in a"duplicate"— no error, no double-counting. - Auto-creates accounts: If the
account_iddoesn't exist, it's created automatically ("live"→ live type,"paper"→ paper type). - Position tracking is automatic: The ledger maintains positions from trade history. Buy trades open/increase positions, sell trades reduce/close them.
- Metadata on positions:
stop_loss,take_profit, andconfidenceare copied from the opening trade to the position.exit_priceandexit_reasonare set when the position closes. - Batch max: 1000 trades per request.
- Timestamps: Always use RFC3339 format (e.g.,
2025-06-15T10:30:00Z).