PR 6: Database migration validation - fresh/upgrade test complete
✅ Database Setup: - Created PostgreSQL kartselldb with kartsell user - SSH port forward established (localhost:5432 → 178.104.200.7:5432) ✅ DbMigrator Fixes: - Fixed migration path discovery (AppContext.BaseDirectory fallback) - Added empty variable dictionary to suppress DbUp preprocessing - Fixed PostgreSQL dollar quoting conflict ($policy$ → $$) ✅ Migration Results: - All 21 migrations executed successfully - Schema versions journal created and tracked - 21 scripts processed in order, no rollback needed Status: FRESH DATABASE DEPLOYMENT SUCCESSFUL - kartselldb fully initialized with v16 schema - Ready for application startup Next: Deploy application and run integration tests Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import { api } from '../../shared/api/client';
|
||||
import { researchSellPolicyRequestSchema, researchSellPolicyResponseSchema } from './schema';
|
||||
export async function evaluateResearchSellPolicy(command) {
|
||||
const request = researchSellPolicyRequestSchema.parse(command.request);
|
||||
const { data } = await api.post('/internal/v1/research/sell-policy/evaluate', request, { headers: { 'Idempotency-Key': command.idempotencyKey } });
|
||||
return researchSellPolicyResponseSchema.parse(data);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { computed } from 'vue';
|
||||
const props = defineProps();
|
||||
const dispositionLabel = { 0: 'NOT_APPLICABLE', 1: 'BLOCKED', 2: 'APPLIED' };
|
||||
const ordered = computed(() => [...props.entries].sort((a, b) => b.priority - a.priority));
|
||||
const __VLS_ctx = {
|
||||
...{},
|
||||
...{},
|
||||
...{},
|
||||
...{},
|
||||
};
|
||||
let __VLS_components;
|
||||
let __VLS_intrinsics;
|
||||
let __VLS_directives;
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.section, __VLS_intrinsics.section)({
|
||||
'aria-labelledby': "policy-trace-title",
|
||||
});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.h2, __VLS_intrinsics.h2)({
|
||||
id: "policy-trace-title",
|
||||
});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.p, __VLS_intrinsics.p)({});
|
||||
(props.schemaVersion);
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.ol, __VLS_intrinsics.ol)({});
|
||||
for (const [entry] of __VLS_vFor((__VLS_ctx.ordered))) {
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.li, __VLS_intrinsics.li)({
|
||||
key: (`${entry.priority}-${entry.policyId}`),
|
||||
});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.strong, __VLS_intrinsics.strong)({});
|
||||
(entry.policyId);
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({});
|
||||
(__VLS_ctx.dispositionLabel[entry.disposition]);
|
||||
(entry.reasonCode);
|
||||
if (entry.requestedSellRatioOfLot > 0) {
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({});
|
||||
(entry.requestedSellRatioOfLot);
|
||||
(entry.appliedSellRatioOfLot);
|
||||
}
|
||||
if (entry.strategicCoreClampApplied) {
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({});
|
||||
}
|
||||
// @ts-ignore
|
||||
[ordered, dispositionLabel,];
|
||||
}
|
||||
// @ts-ignore
|
||||
[];
|
||||
const __VLS_export = (await import('vue')).defineComponent({
|
||||
__typeProps: {},
|
||||
});
|
||||
export default {};
|
||||
@@ -0,0 +1,162 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import QueryStateBoundary from '../../../shared/ui/QueryStateBoundary.vue';
|
||||
import PolicyTracePanel from '../components/PolicyTracePanel.vue';
|
||||
import { useEvaluateResearchSellPolicy } from '../queries';
|
||||
const mutation = useEvaluateResearchSellPolicy();
|
||||
const hardImpairmentApproved = ref(false);
|
||||
const capitalFloorBreached = ref(false);
|
||||
const gapBelowFloorAtr = ref(1.6);
|
||||
const consecutiveCloseBreaches = ref(0);
|
||||
const lastCommand = ref(null);
|
||||
const isBusy = computed(() => mutation.isPending.value);
|
||||
function createCommand() {
|
||||
const asOf = new Date().toISOString();
|
||||
return {
|
||||
idempotencyKey: crypto.randomUUID(),
|
||||
request: {
|
||||
positionLotId: '00000000-0000-0000-0000-000000000001',
|
||||
cycleId: '00000000-0000-0000-0000-000000000002',
|
||||
evidenceId: 'sample-evidence',
|
||||
datasetId: 'sample-dataset',
|
||||
modelVersion: 'research-v12.2',
|
||||
configVersion: 'proposal-v12.2',
|
||||
codeSha: 'sample-code-sha',
|
||||
asOf,
|
||||
publishedAtCutoff: asOf,
|
||||
currentSecurityPortfolioWeight: 0.6,
|
||||
currentLotPortfolioWeight: 0.2,
|
||||
strategicCoreFloorWeight: 0.3,
|
||||
hardImpairmentApproved: hardImpairmentApproved.value,
|
||||
capitalFloorBreached: capitalFloorBreached.value,
|
||||
survivalSellRatioOfLot: 0.5,
|
||||
gapBelowFloorAtr: gapBelowFloorAtr.value,
|
||||
consecutiveCloseBreaches: consecutiveCloseBreaches.value,
|
||||
cooldownSatisfied: true,
|
||||
concentrationSellRatioOfLot: 0,
|
||||
opportunityEdgeLowerBound: 0,
|
||||
opportunitySellRatioOfLot: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
function run() {
|
||||
const command = createCommand();
|
||||
lastCommand.value = command;
|
||||
mutation.mutate(command);
|
||||
}
|
||||
function retry() {
|
||||
if (lastCommand.value)
|
||||
mutation.mutate(lastCommand.value);
|
||||
}
|
||||
const __VLS_ctx = {
|
||||
...{},
|
||||
...{},
|
||||
};
|
||||
let __VLS_components;
|
||||
let __VLS_intrinsics;
|
||||
let __VLS_directives;
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.main, __VLS_intrinsics.main)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.header, __VLS_intrinsics.header)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.h1, __VLS_intrinsics.h1)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.p, __VLS_intrinsics.p)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.strong, __VLS_intrinsics.strong)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.form, __VLS_intrinsics.form)({
|
||||
...{ onSubmit: (__VLS_ctx.run) },
|
||||
});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.fieldset, __VLS_intrinsics.fieldset)({
|
||||
disabled: (__VLS_ctx.isBusy),
|
||||
});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.legend, __VLS_intrinsics.legend)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.label, __VLS_intrinsics.label)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.input)({
|
||||
type: "checkbox",
|
||||
});
|
||||
(__VLS_ctx.hardImpairmentApproved);
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.label, __VLS_intrinsics.label)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.input)({
|
||||
type: "checkbox",
|
||||
});
|
||||
(__VLS_ctx.capitalFloorBreached);
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.label, __VLS_intrinsics.label)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.input)({
|
||||
type: "number",
|
||||
min: "0",
|
||||
step: "0.1",
|
||||
});
|
||||
(__VLS_ctx.gapBelowFloorAtr);
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.label, __VLS_intrinsics.label)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.input)({
|
||||
type: "number",
|
||||
min: "0",
|
||||
step: "1",
|
||||
});
|
||||
(__VLS_ctx.consecutiveCloseBreaches);
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.button, __VLS_intrinsics.button)({
|
||||
type: "submit",
|
||||
});
|
||||
const __VLS_0 = QueryStateBoundary || QueryStateBoundary;
|
||||
// @ts-ignore
|
||||
const __VLS_1 = __VLS_asFunctionalComponent1(__VLS_0, new __VLS_0({
|
||||
...{ 'onRetry': {} },
|
||||
loading: (__VLS_ctx.isBusy),
|
||||
error: __VLS_ctx.mutation.error.value,
|
||||
empty: (!__VLS_ctx.mutation.data.value),
|
||||
}));
|
||||
const __VLS_2 = __VLS_1({
|
||||
...{ 'onRetry': {} },
|
||||
loading: (__VLS_ctx.isBusy),
|
||||
error: __VLS_ctx.mutation.error.value,
|
||||
empty: (!__VLS_ctx.mutation.data.value),
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_1));
|
||||
let __VLS_5;
|
||||
const __VLS_6 = {
|
||||
/** @type {typeof __VLS_5.retry} */
|
||||
onRetry: (__VLS_ctx.retry),
|
||||
};
|
||||
const { default: __VLS_7 } = __VLS_3.slots;
|
||||
if (__VLS_ctx.mutation.data.value) {
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dl, __VLS_intrinsics.dl)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
(__VLS_ctx.mutation.data.value.action);
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
(__VLS_ctx.mutation.data.value.policyId);
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
(__VLS_ctx.mutation.data.value.reasonCode);
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
(__VLS_ctx.mutation.data.value.sellRatioOfLot);
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
(__VLS_ctx.mutation.data.value.targetSecurityPortfolioWeightAfter);
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
(__VLS_ctx.mutation.data.value.reentryEligible);
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
(__VLS_ctx.mutation.data.value.decisionContractVersion);
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
(__VLS_ctx.mutation.data.value.policyTrace.length);
|
||||
}
|
||||
if (__VLS_ctx.mutation.data.value) {
|
||||
const __VLS_8 = PolicyTracePanel;
|
||||
// @ts-ignore
|
||||
const __VLS_9 = __VLS_asFunctionalComponent1(__VLS_8, new __VLS_8({
|
||||
entries: (__VLS_ctx.mutation.data.value.policyTrace),
|
||||
schemaVersion: (__VLS_ctx.mutation.data.value.policyTraceSchemaVersion),
|
||||
}));
|
||||
const __VLS_10 = __VLS_9({
|
||||
entries: (__VLS_ctx.mutation.data.value.policyTrace),
|
||||
schemaVersion: (__VLS_ctx.mutation.data.value.policyTraceSchemaVersion),
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_9));
|
||||
}
|
||||
// @ts-ignore
|
||||
[run, isBusy, isBusy, hardImpairmentApproved, capitalFloorBreached, gapBelowFloorAtr, consecutiveCloseBreaches, mutation, mutation, mutation, mutation, mutation, mutation, mutation, mutation, mutation, mutation, mutation, mutation, mutation, mutation, retry,];
|
||||
var __VLS_3;
|
||||
var __VLS_4;
|
||||
// @ts-ignore
|
||||
[];
|
||||
const __VLS_export = (await import('vue')).defineComponent({});
|
||||
export default {};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { useMutation } from '@tanstack/vue-query';
|
||||
import { evaluateResearchSellPolicy } from './api';
|
||||
export function useEvaluateResearchSellPolicy() {
|
||||
return useMutation({ mutationFn: evaluateResearchSellPolicy });
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { z } from 'zod';
|
||||
const ratio = z.number().min(0).max(1);
|
||||
const policyTraceEntrySchema = z.object({
|
||||
policyId: z.string().min(1),
|
||||
priority: z.number().int(),
|
||||
disposition: z.union([z.literal(0), z.literal(1), z.literal(2)]),
|
||||
reasonCode: z.string().min(1),
|
||||
requestedSellRatioOfLot: ratio,
|
||||
appliedSellRatioOfLot: ratio,
|
||||
strategicCoreClampApplied: z.boolean()
|
||||
});
|
||||
export const researchSellPolicyRequestSchema = z.object({
|
||||
positionLotId: z.string().uuid(),
|
||||
cycleId: z.string().uuid(),
|
||||
evidenceId: z.string().min(1).max(128),
|
||||
datasetId: z.string().min(1).max(128),
|
||||
modelVersion: z.string().min(1).max(128),
|
||||
configVersion: z.string().min(1).max(128),
|
||||
codeSha: z.string().min(1).max(128),
|
||||
asOf: z.string().datetime({ offset: true }),
|
||||
publishedAtCutoff: z.string().datetime({ offset: true }),
|
||||
currentSecurityPortfolioWeight: ratio,
|
||||
currentLotPortfolioWeight: ratio,
|
||||
strategicCoreFloorWeight: ratio,
|
||||
hardImpairmentApproved: z.boolean(),
|
||||
capitalFloorBreached: z.boolean(),
|
||||
survivalSellRatioOfLot: ratio,
|
||||
gapBelowFloorAtr: z.number().min(0),
|
||||
consecutiveCloseBreaches: z.number().int().min(0),
|
||||
cooldownSatisfied: z.boolean(),
|
||||
concentrationSellRatioOfLot: ratio,
|
||||
opportunityEdgeLowerBound: z.number(),
|
||||
opportunitySellRatioOfLot: ratio
|
||||
}).superRefine((value, ctx) => {
|
||||
if (value.currentLotPortfolioWeight > value.currentSecurityPortfolioWeight) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Lot 비중은 종목 전체 비중을 초과할 수 없습니다.',
|
||||
path: ['currentLotPortfolioWeight']
|
||||
});
|
||||
}
|
||||
if (new Date(value.publishedAtCutoff) > new Date(value.asOf)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: '공개 가능 시각은 평가 시각 이후일 수 없습니다.',
|
||||
path: ['publishedAtCutoff']
|
||||
});
|
||||
}
|
||||
});
|
||||
export const researchSellPolicyResponseSchema = z.object({
|
||||
action: z.enum(['Hold', 'PartialSell', 'FullSell']),
|
||||
sellRatioOfLot: ratio,
|
||||
targetSecurityPortfolioWeightAfter: ratio,
|
||||
policyId: z.string().min(1),
|
||||
reasonCode: z.string().min(1),
|
||||
decisionContractVersion: z.literal('sell-decision.v2'),
|
||||
policyTraceSchemaVersion: z.literal(2),
|
||||
reentryEligible: z.boolean(),
|
||||
policyTrace: z.array(policyTraceEntrySchema),
|
||||
evidenceStatus: z.literal('RESEARCH_CANDIDATE_NOT_PRODUCTION')
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { researchSellPolicyRequestSchema, researchSellPolicyResponseSchema } from '../schema';
|
||||
const baseRequest = {
|
||||
positionLotId: '00000000-0000-0000-0000-000000000001',
|
||||
cycleId: '00000000-0000-0000-0000-000000000002',
|
||||
evidenceId: 'evidence-1',
|
||||
datasetId: 'dataset-1',
|
||||
modelVersion: 'model-1',
|
||||
configVersion: 'config-1',
|
||||
codeSha: 'sha-1',
|
||||
asOf: '2026-08-01T07:00:00Z',
|
||||
publishedAtCutoff: '2026-08-01T06:00:00Z',
|
||||
currentSecurityPortfolioWeight: 0.6,
|
||||
currentLotPortfolioWeight: 0.2,
|
||||
strategicCoreFloorWeight: 0.3,
|
||||
hardImpairmentApproved: false,
|
||||
capitalFloorBreached: false,
|
||||
survivalSellRatioOfLot: 0,
|
||||
gapBelowFloorAtr: 0,
|
||||
consecutiveCloseBreaches: 0,
|
||||
cooldownSatisfied: true,
|
||||
concentrationSellRatioOfLot: 0,
|
||||
opportunityEdgeLowerBound: 0,
|
||||
opportunitySellRatioOfLot: 0
|
||||
};
|
||||
describe('research sell policy contracts', () => {
|
||||
it('accepts a valid point-in-time request', () => {
|
||||
expect(researchSellPolicyRequestSchema.safeParse(baseRequest).success).toBe(true);
|
||||
});
|
||||
it('rejects a lot weight above security weight', () => {
|
||||
const result = researchSellPolicyRequestSchema.safeParse({
|
||||
...baseRequest,
|
||||
currentLotPortfolioWeight: 0.7
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
it('rejects a future published-at cutoff', () => {
|
||||
const result = researchSellPolicyRequestSchema.safeParse({
|
||||
...baseRequest,
|
||||
publishedAtCutoff: '2026-08-01T08:00:00Z'
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
it('rejects an unknown production status', () => {
|
||||
const result = researchSellPolicyResponseSchema.safeParse({
|
||||
action: 'Hold',
|
||||
sellRatioOfLot: 0,
|
||||
targetSecurityPortfolioWeightAfter: 0.6,
|
||||
policyId: 'ALG-HOLD-001',
|
||||
reasonCode: 'NO_SELL_CONDITION',
|
||||
reentryEligible: false,
|
||||
policyTrace: [],
|
||||
evidenceStatus: 'PRODUCTION'
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user