feat: strategy builder

This commit is contained in:
OpenSquared
2026-07-18 16:37:35 +02:00
parent 16ccc7c2c7
commit 91054979ec
14 changed files with 2106 additions and 2 deletions

View File

@@ -1515,3 +1515,152 @@ export const useRejectAiTradeProposal = () => {
onSuccess: () => qc.invalidateQueries({ queryKey: ['ai-trade-proposals'] }),
})
}
// ── Strategy Builder ────────────────────────────────────────────────────────
export type ChainRow = { strike: number; bid: number; ask: number; mid: number; last: number; iv: number; open_interest: number; volume: number }
export type ChainExpiry = { expiry_date: string; days_to_expiry: number; calls: ChainRow[]; puts: ChainRow[] }
export type ChainSlice = { symbol: string; proxy: string; spot: number; expiries: ChainExpiry[] }
export type ManualGridCell = { days_to_expiry: number; strike_pct: number; iv: number | null }
export type StrategyScenario = {
symbol: string
horizon_days: number
spot_shock_pct: number
iv_level_shift: number
skew_tilt: number
term_shift: number
manual_grid?: ManualGridCell[]
rate?: number
n_expiries?: number
}
export type StrategyLeg = {
expiry_date: string
days_to_expiry: number
strike: number
option_type: 'call' | 'put'
position: 'long' | 'short'
quantity: number
}
export type Greeks = { delta: number; gamma: number; theta: number; vega: number }
export type PayoffPoint = { underlying: number; pnl: number }
export type PriceCombo = {
entry_cost: number
entry_cost_mid: number
scenario_value: number
scenario_value_mid: number
net_pnl: number
broker_spread_cost: number
max_gain: number | null
max_loss: number | null
bounded_risk: boolean
greeks_now: Greeks
greeks_scenario: Greeks
net_delta_now: number
net_delta_scenario: number
at_expiry: PayoffPoint[]
at_scenario: PayoffPoint[]
spot: number
scenario_spot: number
proxy: string
}
export type StrategyCandidate = {
template_name: string
legs: StrategyLeg[]
score: number
objective: string
} & PriceCombo
export const useOptionChainSlice = (symbol: string, horizonDays: number, nExpiries = 3, enabled = true) =>
useQuery<ChainSlice>({
queryKey: ['strategy-builder-chain', symbol, horizonDays, nExpiries],
queryFn: () => api.get('/strategy-builder/chain', { params: { symbol, horizon_days: horizonDays, n_expiries: nExpiries } }).then(r => r.data),
enabled: enabled && !!symbol,
staleTime: 30_000,
retry: 1,
})
export const usePriceStrategy = () =>
useMutation({
mutationFn: (body: { scenario: StrategyScenario; legs: StrategyLeg[] }) =>
api.post<PriceCombo>('/strategy-builder/price', body).then(r => r.data),
})
export type OptimizeConstraints = {
max_legs: number
delta_threshold: number
max_loss_cap?: number | null
objective: 'net_pnl' | 'return_on_risk' | 'prob_weighted'
top_n?: number
}
export const useOptimizeStrategy = () =>
useMutation({
mutationFn: (body: { scenario: StrategyScenario; constraints: OptimizeConstraints }) =>
api.post<StrategyCandidate[]>('/strategy-builder/optimize', body).then(r => r.data),
})
export type SavedScenario = {
id: string; symbol: string; label: string; horizon_days: number
spot_shock_pct: number; iv_level_shift: number; skew_tilt: number; term_shift: number
manual_grid: ManualGridCell[]; created_at: string
}
export type SavedStrategyRecord = {
id: string; scenario_id: string | null; symbol: string; template_name: string; objective: string
legs: StrategyLeg[]; entry_cost: number | null; max_gain: number | null; max_loss: number | null
net_pnl_scenario: number | null; net_delta: number | null; notes: string; created_at: string
}
export const useScenarios = (symbol?: string) =>
useQuery<SavedScenario[]>({
queryKey: ['strategy-scenarios', symbol],
queryFn: () => api.get('/strategy-builder/scenarios', { params: symbol ? { symbol } : {} }).then(r => r.data),
})
export const useSaveScenario = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (body: StrategyScenario & { label?: string }) => api.post('/strategy-builder/scenarios', body).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['strategy-scenarios'] }),
})
}
export const useDeleteScenario = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (id: string) => api.delete(`/strategy-builder/scenarios/${id}`).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['strategy-scenarios'] }),
})
}
export const useSavedStrategies = (symbol?: string) =>
useQuery<SavedStrategyRecord[]>({
queryKey: ['saved-strategies', symbol],
queryFn: () => api.get('/strategy-builder/saved', { params: symbol ? { symbol } : {} }).then(r => r.data),
})
export const useSaveStrategyRecord = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (body: {
scenario_id?: string | null; symbol: string; template_name?: string; objective?: string
legs: StrategyLeg[]; entry_cost?: number | null; max_gain?: number | null; max_loss?: number | null
net_pnl_scenario?: number | null; net_delta?: number | null; notes?: string
}) => api.post('/strategy-builder/saved', body).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['saved-strategies'] }),
})
}
export const useDeleteSavedStrategy = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (id: string) => api.delete(`/strategy-builder/saved/${id}`).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['saved-strategies'] }),
})
}