candle-annotator/openspec/changes/multi-chart-management/specs/backend-api/spec.md
Marko Djordjevic 92d3339a48 feat: add charts table schema and migration with data backfill
- Add charts table with id, name (unique), created_at
- Add chart_id FK to candles table with composite unique on (chart_id, time)
- Add chart_id FK to annotations table
- Custom migration handles existing data: creates 'Imported Data' chart and backfills chart_id
- Recreates tables for NOT NULL constraint (SQLite limitation)
2026-02-13 00:12:21 +01:00

6.1 KiB

MODIFIED Requirements

Requirement: Upload endpoint

The system SHALL provide a POST /api/upload endpoint that accepts a CSV file via multipart form data. The endpoint SHALL create a new chart (named from the uploaded filename without extension), parse the CSV using papaparse, validate the format, and insert all candle records into the candles table with the new chart's chart_id within a single database transaction. On success, the endpoint SHALL return a JSON response with the chart id, chart name, and the count of inserted records. On failure, it SHALL return an appropriate error status and message without creating a chart.

Scenario: Successful upload

  • WHEN a valid CSV file named "BTC-1H.csv" is sent to POST /api/upload
  • THEN endpoint creates a chart named "BTC-1H", inserts candles with that chart_id, and returns { "success": true, "count": <number>, "chart": { "id": <number>, "name": "BTC-1H" } } with HTTP 200

Scenario: Invalid CSV upload

  • WHEN a CSV with missing or invalid headers is sent to POST /api/upload
  • THEN endpoint returns { "error": "<description>" } with HTTP 400 and does not create a chart

Scenario: No file provided

  • WHEN POST /api/upload is called without a file
  • THEN endpoint returns { "error": "No file provided" } with HTTP 400

Scenario: Duplicate filename

  • WHEN a CSV named "BTC-1H.csv" is uploaded and a chart named "BTC-1H" already exists
  • THEN endpoint creates a chart named "BTC-1H-2" (or next available suffix) and inserts candles under that chart

Requirement: Get annotations endpoint

The system SHALL provide a GET /api/annotations endpoint that accepts an optional chartId query parameter. When chartId is provided, the endpoint SHALL return only annotations belonging to that chart. When chartId is omitted, the endpoint SHALL return annotations for the most recently created chart. Each annotation object SHALL include: id, chart_id, timestamp, label_type, geometry (parsed from JSON string or null), and created_at.

Scenario: Fetch annotations for specific chart

  • WHEN GET /api/annotations?chartId=3 is called
  • THEN endpoint returns a JSON array of annotations where chart_id equals 3, with HTTP 200

Scenario: Fetch annotations without chartId

  • WHEN GET /api/annotations is called without a chartId parameter
  • THEN endpoint returns annotations for the most recently created chart with HTTP 200

Scenario: No annotations exist for chart

  • WHEN GET /api/annotations?chartId=3 is called and no annotations exist for chart 3
  • THEN endpoint returns an empty JSON array [] with HTTP 200

Requirement: Create annotation endpoint

The system SHALL provide a POST /api/annotations endpoint that accepts a JSON body with fields: timestamp (required, integer), label_type (required, string), chart_id (required, integer), and geometry (optional, object). The endpoint SHALL validate the input, verify the chart exists, serialize geometry to JSON string if present, and insert the record into the annotations table. On success, it SHALL return the created annotation object with its assigned id.

Scenario: Create a marker annotation

  • WHEN POST /api/annotations is called with { "timestamp": 1700000000, "label_type": "break_up", "chart_id": 3 }
  • THEN endpoint saves the annotation with chart_id 3 and returns the created object with id and HTTP 201

Scenario: Create a line annotation

  • WHEN POST /api/annotations is called with { "timestamp": 1700000000, "label_type": "line", "chart_id": 3, "geometry": { "startTime": 1700000000, "startPrice": 1.05, "endTime": 1700100000, "endPrice": 1.06 } }
  • THEN endpoint saves the annotation with chart_id 3 and serialized geometry JSON, returns the created object with HTTP 201

Scenario: Invalid annotation data

  • WHEN POST /api/annotations is called with missing required fields (timestamp, label_type, or chart_id)
  • THEN endpoint returns { "error": "<description>" } with HTTP 400

Scenario: Annotation for non-existent chart

  • WHEN POST /api/annotations is called with a chart_id that does not exist
  • THEN endpoint returns { "error": "Chart not found" } with HTTP 404

Requirement: Get candles endpoint

The system SHALL provide a GET /api/candles endpoint that accepts an optional chartId query parameter. When chartId is provided, the endpoint SHALL return only candles belonging to that chart. When chartId is omitted, the endpoint SHALL return candles for the most recently created chart. Results SHALL be ordered by time ascending. Each object SHALL include: time, open, high, low, close.

Scenario: Fetch candles for specific chart

  • WHEN GET /api/candles?chartId=3 is called
  • THEN endpoint returns a JSON array of candle objects where chart_id equals 3, ordered by time ascending with HTTP 200

Scenario: Fetch candles without chartId

  • WHEN GET /api/candles is called without a chartId parameter
  • THEN endpoint returns candles for the most recently created chart with HTTP 200

Scenario: No candles exist for chart

  • WHEN GET /api/candles?chartId=3 is called and no candles exist for chart 3
  • THEN endpoint returns an empty JSON array [] with HTTP 200

Requirement: Export annotations endpoint

The system SHALL provide a GET /api/export endpoint that accepts an optional chartId query parameter. When chartId is provided, the endpoint SHALL export only annotations for that chart. When chartId is omitted, the endpoint SHALL export annotations for the most recently created chart. The CSV SHALL have columns: timestamp, label_type, price. The response SHALL set Content-Type: text/csv and Content-Disposition: attachment; filename="annotations.csv" headers.

Scenario: Export for specific chart

  • WHEN GET /api/export?chartId=3 is called and annotations exist for chart 3
  • THEN endpoint returns a CSV file download with only annotations belonging to chart 3

Scenario: Export without chartId

  • WHEN GET /api/export is called without a chartId parameter
  • THEN endpoint exports annotations for the most recently created chart