Skip to content

Commit 725299a

Browse files
authored
Merge pull request #989 from tradingstrategy-ai/pair-selector-enhancements
PairSelector enhancementss
2 parents 7be6c02 + 35c78a9 commit 725299a

4 files changed

Lines changed: 107 additions & 30 deletions

File tree

src/lib/trade-executor/schemas/chart.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const chartKind = z.enum([
1717
'state_single_pair',
1818
'state_single_vault_pair'
1919
]);
20+
export type ChartKind = z.infer<typeof chartKind>;
2021

2122
export const chartRegistrationSchema = z.object({
2223
id: z.string(),

src/routes/strategies/[strategy]/tech-details/analysis/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
<PairsSelector
3434
{selectedPairIds}
3535
{tradingPairs}
36-
disabled={selectedChart?.kind !== 'indicator_multi_pair'}
36+
chartKind={selectedChart?.kind}
3737
onchange={(pair_ids) => updateAnalysis({ pair_ids })}
3838
/>
3939
</div>

src/routes/strategies/[strategy]/tech-details/analysis/+page.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ChartRegistration, TradingPairs } from 'trade-executor/schemas/chart.js';
1+
import type { ChartRegistration } from 'trade-executor/schemas/chart.js';
22

33
// Use discriminated union so data type is correctly inferred
44
type AnalysisContent =
@@ -17,12 +17,10 @@ async function fetchAnalysisContent(
1717
chartRegistration: ChartRegistration,
1818
pairIds: number[]
1919
): Promise<AnalysisContent> {
20-
const params = new URLSearchParams({ chart_id: chartRegistration.id });
21-
22-
// add pair_ids param if required by the chart kind
23-
if (chartRegistration.kind === 'indicator_multi_pair') {
24-
params.set('pair_ids', pairIds.join(','));
25-
}
20+
const params = new URLSearchParams({
21+
chart_id: chartRegistration.id,
22+
pair_ids: pairIds.join(',')
23+
});
2624

2725
const response = await fetch(`${strategyUrl}/chart-registry/render?${params}`);
2826
const type = response.headers.get('content-type') ?? 'unknown';
@@ -62,5 +60,5 @@ export async function load({ fetch, parent, url }) {
6260

6361
const contentPromise = selectedChart && fetchAnalysisContent(fetch, strategy.url, selectedChart, selectedPairIds);
6462

65-
return { chartRegistrations, selectedChart, selectedPairIds, contentPromise };
63+
return { selectedChart, selectedPairIds, contentPromise };
6664
}

src/routes/strategies/[strategy]/tech-details/analysis/PairsSelector.svelte

Lines changed: 99 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,94 @@
11
<script lang="ts">
2-
import type { ChartPairs, TradingPairs } from 'trade-executor/schemas/chart';
2+
import type { ChartKind, ChartPairs, TradingPairs } from 'trade-executor/schemas/chart';
33
import { slide } from 'svelte/transition';
44
import fsm from 'svelte-fsm';
55
import Button from '$lib/components/Button.svelte';
6+
import TextInput from '$lib/components/TextInput.svelte';
67
78
interface Props {
89
selectedPairIds: number[];
910
tradingPairs: ChartPairs;
10-
disabled?: boolean;
11+
chartKind: ChartKind | undefined;
1112
onchange?: (ids: number[]) => void;
1213
}
1314
14-
let { selectedPairIds, tradingPairs, disabled = false, onchange }: Props = $props();
15+
let { selectedPairIds, tradingPairs, chartKind, onchange }: Props = $props();
16+
17+
let singlePair = $derived(chartKind?.includes('_single_'));
18+
let multiPair = $derived(chartKind?.includes('_multi_'));
19+
let disabled = $derived(!(singlePair || multiPair));
1520
1621
// selected pair ids during editing, prior to committing (save) or reverting (cancel)
17-
let provisionalPairIds = $state(selectedPairIds);
22+
let multiPairIds = $derived(selectedPairIds);
23+
let singlePairId = $derived(selectedPairIds[0]);
1824
1925
let provisionalPairs = $derived(
2026
tradingPairs.all_pairs.filter((p) => {
21-
return provisionalPairIds.includes(p.internal_id!);
27+
if (singlePair) {
28+
return p.internal_id! === singlePairId;
29+
}
30+
return multiPairIds.includes(p.internal_id!);
2231
})
2332
);
2433
34+
let search = $state('');
35+
2536
const pairSelector = fsm('ready', {
2637
ready: {
27-
edit: 'editing'
38+
_enter() {
39+
search = '';
40+
},
41+
42+
edit() {
43+
return singlePair ? 'editingSingle' : 'editingMulti';
44+
}
45+
},
46+
47+
editingSingle: {
48+
select(pairs: TradingPairs) {
49+
singlePairId = pairs[0]?.internal_id;
50+
},
51+
52+
save() {
53+
onchange?.([singlePairId]);
54+
return 'ready';
55+
},
56+
57+
cancel() {
58+
singlePairId = selectedPairIds[0];
59+
return 'ready';
60+
}
2861
},
2962
30-
editing: {
63+
editingMulti: {
3164
select(pairs: TradingPairs) {
32-
provisionalPairIds = pairs.map((p) => p.internal_id!);
65+
multiPairIds = pairs.map((p) => p.internal_id!);
3366
},
3467
3568
save() {
36-
onchange?.(provisionalPairIds);
69+
onchange?.(multiPairIds);
3770
return 'ready';
3871
},
3972
4073
cancel() {
41-
provisionalPairIds = selectedPairIds;
74+
multiPairIds = selectedPairIds;
4275
return 'ready';
4376
}
4477
}
4578
});
4679
47-
let editing = $derived($pairSelector === 'editing');
80+
let editing = $derived($pairSelector.startsWith('editing'));
4881
</script>
4982

5083
<div class="pairs-selector">
5184
<label class={['current-selection', editing && 'editing', disabled && 'disabled']}>
52-
<span class="title">Pairs:</span>
85+
<span class="title">
86+
{singlePair ? 'Pair' : 'Pairs'}:
87+
</span>
5388
<span class="selected-pairs">
54-
{#if tradingPairs.all_pairs.length === 0}
89+
{#if disabled}
90+
No pairs required
91+
{:else if tradingPairs.all_pairs.length === 0}
5592
No pairs loaded
5693
{:else if provisionalPairs.length === 0}
5794
No pairs selected
@@ -67,20 +104,29 @@
67104
<div class="inner">
68105
<header>
69106
<h4>Select pairs</h4>
70-
<div class="button-group">
107+
<div class="button-group quick-select">
71108
<Button size="xs" tertiary on:click={() => pairSelector.select(tradingPairs.default_pairs)}>Default</Button>
72-
<Button size="xs" tertiary on:click={() => pairSelector.select(tradingPairs.all_pairs)}>All</Button>
109+
{#if multiPair}
110+
<Button size="xs" tertiary on:click={() => pairSelector.select(tradingPairs.all_pairs)}>All</Button>
111+
{/if}
73112
<Button size="xs" tertiary on:click={() => pairSelector.select([])}>None</Button>
74113
</div>
75-
<div class="button-group">
114+
<div class="search">
115+
<TextInput bind:value={search} type="search" size="sm" placeholder="Search" />
116+
</div>
117+
<div class="button-group primary-controls">
76118
<Button size="xs" ghost on:click={pairSelector.cancel}>Cancel</Button>
77-
<Button size="xs" secondary on:click={pairSelector.save}>Save</Button>
119+
<Button size="xs" secondary disabled={multiPairIds.length === 0} on:click={pairSelector.save}>Save</Button>
78120
</div>
79121
</header>
80122
<div class="pairs">
81123
{#each tradingPairs.all_pairs as pair (pair.internal_id)}
82-
<label>
83-
<input type="checkbox" value={pair.internal_id} bind:group={provisionalPairIds} />
124+
<label hidden={!pair.symbol.toLowerCase().includes(search.toLowerCase())}>
125+
{#if singlePair}
126+
<input type="radio" value={pair.internal_id} bind:group={singlePairId} />
127+
{:else}
128+
<input type="checkbox" value={pair.internal_id} bind:group={multiPairIds} />
129+
{/if}
84130
{pair.symbol}
85131
</label>
86132
{/each}
@@ -155,12 +201,40 @@
155201
156202
header {
157203
display: grid;
158-
grid-template-columns: auto 1fr auto;
159-
gap: 1.5rem;
204+
grid-template-columns:
205+
[title] auto
206+
[quick-select] 1fr
207+
[search] auto
208+
[primary-controls] auto;
209+
grid-template-rows: auto;
210+
gap: 0.75rem 1.25rem;
160211
align-items: center;
161212
213+
@media (--viewport-sm-down) {
214+
grid-template-columns:
215+
[title quick-select] auto
216+
[search primary-controls] 1fr;
217+
}
218+
162219
h4 {
220+
grid-column-start: title;
163221
font: var(--f-heading-xs-medium);
222+
white-space: nowrap;
223+
}
224+
225+
.search {
226+
grid-column-start: search;
227+
text-align: right;
228+
}
229+
230+
.quick-select {
231+
grid-column-start: quick-select;
232+
}
233+
234+
.primary-controls {
235+
grid-row-start: 1;
236+
grid-column-start: primary-controls;
237+
justify-content: end;
164238
}
165239
166240
.button-group {
@@ -188,6 +262,10 @@
188262
&:hover {
189263
background: var(--c-box-2);
190264
}
265+
266+
&[hidden] {
267+
display: none;
268+
}
191269
}
192270
}
193271
}

0 commit comments

Comments
 (0)