feat: add database schema, migrations, and API endpoints for span annotations

- Add span_label_types and span_annotations tables to schema
- Seed default span label types (bull_flag, bear_flag, etc.)
- Implement CRUD API endpoints for span label types
- Implement CRUD API endpoints for span annotations
- Add time swap validation in POST endpoint (start_time <= end_time)
This commit is contained in:
Marko Djordjevic 2026-02-14 05:56:28 +01:00
parent 8a7eb1fb08
commit dadf515406
11 changed files with 1131 additions and 0 deletions

View file

@ -38,3 +38,28 @@ export const annotations = sqliteTable('annotations', {
color: text('color').default('#3b82f6'), // hex color code
created_at: integer('created_at').notNull(),
});
export const spanLabelTypes = sqliteTable('span_label_types', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull().unique(), // internal name (e.g., 'bull_flag')
display_name: text('display_name').notNull(), // UI label (e.g., 'Bull Flag')
color: text('color').notNull(), // hex color for rectangle fill
hotkey: text('hotkey'), // keyboard shortcut (e.g., '1')
is_active: integer('is_active').notNull().default(1), // 1 = active, 0 = inactive
sort_order: integer('sort_order').notNull().default(0), // display order
created_at: integer('created_at').notNull(),
});
export const spanAnnotations = sqliteTable('span_annotations', {
id: integer('id').primaryKey({ autoIncrement: true }),
chart_id: integer('chart_id').notNull().references(() => charts.id),
start_time: integer('start_time').notNull(), // Unix timestamp of first candle
end_time: integer('end_time').notNull(), // Unix timestamp of last candle
label: text('label').notNull(), // pattern name referencing span_label_types.name
confidence: integer('confidence'), // 1-5 scale, nullable
outcome: text('outcome'), // 'win'|'loss'|'breakeven'|null
notes: text('notes'), // free-text, nullable
sub_spans: text('sub_spans'), // JSON array of sub-span objects, nullable
color: text('color').notNull().default('#2196F3'), // hex color
created_at: integer('created_at').notNull(),
});

View file

@ -0,0 +1,82 @@
import { db } from './index';
import { spanLabelTypes } from './schema';
export async function seedSpanLabelTypes() {
const now = Math.floor(Date.now() / 1000);
const defaultTypes = [
{
name: 'bull_flag',
display_name: 'Bull Flag',
color: '#4CAF50',
hotkey: '1',
is_active: 1,
sort_order: 1,
created_at: now,
},
{
name: 'bear_flag',
display_name: 'Bear Flag',
color: '#F44336',
hotkey: '2',
is_active: 1,
sort_order: 2,
created_at: now,
},
{
name: 'head_and_shoulders',
display_name: 'Head and Shoulders',
color: '#9C27B0',
hotkey: '3',
is_active: 1,
sort_order: 3,
created_at: now,
},
{
name: 'double_bottom',
display_name: 'Double Bottom',
color: '#2196F3',
hotkey: '4',
is_active: 1,
sort_order: 4,
created_at: now,
},
{
name: 'wedge_up',
display_name: 'Wedge Up',
color: '#FF9800',
hotkey: '5',
is_active: 1,
sort_order: 5,
created_at: now,
},
{
name: 'wedge_down',
display_name: 'Wedge Down',
color: '#FF5722',
hotkey: '6',
is_active: 1,
sort_order: 6,
created_at: now,
},
{
name: 'custom',
display_name: 'Custom',
color: '#607D8B',
hotkey: '7',
is_active: 1,
sort_order: 7,
created_at: now,
},
];
// Check if types already exist
const existing = await db.select().from(spanLabelTypes);
if (existing.length === 0) {
await db.insert(spanLabelTypes).values(defaultTypes);
console.log('Seeded default span label types');
} else {
console.log('Span label types already exist, skipping seed');
}
}