跳至主要内容

创建投资

StableFX 中的外汇投资是有时间限制的仓位,资金被分配用于在特定周期内交易某个货币对。本页介绍 useForexInvestments hook、投资类型、周期选项和费用结构。

核心类型

ForexInvestment

import type { ForexInvestment } from '@one_deploy/sdk';

interface ForexInvestment {
/** Unique investment identifier. */
id: string;

/** The currency pair being traded. */
currencyPair: string;

/** Total amount deposited (in USDC). */
amount: number;

/** Amount allocated to active trading. */
tradingCapital: number;

/** Amount allocated to pool reserves. */
reserveCapital: number;

/** The selected cycle duration. */
cycle: string;

/** Current status of the investment. */
status: 'active' | 'pending' | 'completed' | 'cancelled';

/** Realized profit or loss (in USDC). Updated during the cycle. */
pnl: number;

/** Current return as a decimal (e.g. 0.05 = 5%). */
returnRate: number;

/** Total fees charged (in USDC). */
feesCharged: number;

/** ISO-8601 timestamp when the investment was created. */
createdAt: string;

/** ISO-8601 timestamp when the cycle ends. */
expiresAt: string;

/** ISO-8601 timestamp of last update. */
updatedAt: string;
}

ForexCycleOption

周期选项定义了可用的投资期限。每个周期有不同的费率和预期收益范围。

import type { ForexCycleOption } from '@one_deploy/sdk';

interface ForexCycleOption {
/** Cycle identifier, e.g. "7d", "14d", "30d". */
id: string;

/** Human-readable label. */
label: string;

/** Duration in days. */
durationDays: number;

/** Management fee as a decimal (e.g. 0.01 = 1%). */
managementFee: number;

/** Performance fee as a decimal, charged on profits only. */
performanceFee: number;

/** Minimum investment amount in USDC. */
minimumInvestment: number;

/** Whether this cycle option is currently available. */
isAvailable: boolean;
}

可用周期

周期时长管理费绩效费最低投资额
7d7 天0.5%10%$100
14d14 天0.75%10%$250
30d30 天1.0%15%$500
60d60 天1.5%15%$1,000
90d90 天2.0%20%$2,500

费用结构

费用通过两种方式从投资中扣除:

  1. 管理费 -- 在总存款金额上预先收取。该费用用于运营成本和池维护。
  2. 绩效费 -- 仅在周期结束时对已实现利润收取。如果投资产生亏损,则不收取绩效费。
// Fee calculation example for a $10,000 investment on a 30-day cycle
const deposit = 10_000;
const managementFee = deposit * 0.01; // $100 (1.0%)
const netDeposit = deposit - managementFee; // $9,900

// If the cycle generates $500 profit:
const profit = 500;
const performanceFee = profit * 0.15; // $75 (15% of profit)
const netProfit = profit - performanceFee; // $425

// Total return to user: $9,900 + $425 = $10,325

useForexInvestments Hook

Hook 签名

import { useForexInvestments } from '@one_deploy/sdk';

function useForexInvestments(): UseForexInvestmentsResult;

UseForexInvestmentsResult

interface UseForexInvestmentsResult {
/** All investments for the current user. */
investments: ForexInvestment[];

/** Available cycle options. */
cycleOptions: ForexCycleOption[];

/** Whether the initial fetch is in progress. */
isLoading: boolean;

/** Error object if the fetch failed. */
error: Error | null;

/** Create a new forex investment. */
createInvestment: (params: CreateInvestmentParams) => Promise<ForexInvestment>;

/** Cancel an active or pending investment. */
cancelInvestment: (investmentId: string) => Promise<void>;

/** Re-fetch investments and cycle options. */
refetch: () => Promise<void>;
}

interface CreateInvestmentParams {
/** Deposit amount in USDC. */
amount: number;

/** Currency pair to trade, e.g. "EUR/USD". */
currencyPair: string;

/** Cycle identifier, e.g. "7d", "30d". */
cycle: string;
}

创建外汇投资

基础示例

import React, { useState } from 'react';
import { View, Text, TouchableOpacity, Alert } from 'react-native';
import { useForexInvestments } from '@one_deploy/sdk';

function CreateInvestment() {
const {
createInvestment,
cycleOptions,
isLoading,
error,
} = useForexInvestments();

const [submitting, setSubmitting] = useState(false);

const handleCreate = async () => {
setSubmitting(true);
try {
const investment = await createInvestment({
amount: 5000,
currencyPair: 'EUR/USD',
cycle: '30d',
});

Alert.alert(
'Investment Created',
`ID: ${investment.id}\n` +
`Trading capital: $${investment.tradingCapital}\n` +
`Expires: ${new Date(investment.expiresAt).toLocaleDateString()}`
);
} catch (err) {
Alert.alert('Error', (err as Error).message);
} finally {
setSubmitting(false);
}
};

return (
<View style={{ padding: 16 }}>
<Text style={{ fontSize: 18, fontWeight: '700', color: '#fff' }}>
New Forex Investment
</Text>
<TouchableOpacity
onPress={handleCreate}
disabled={submitting || isLoading}
style={{
backgroundColor: '#4488ff',
padding: 14,
borderRadius: 8,
marginTop: 16,
opacity: submitting ? 0.6 : 1,
}}
>
<Text style={{ color: '#fff', textAlign: 'center', fontWeight: '600' }}>
{submitting ? 'Creating...' : 'Invest $5,000 in EUR/USD (30d)'}
</Text>
</TouchableOpacity>
</View>
);
}

带周期选择的完整投资表单

import React, { useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
ScrollView,
StyleSheet,
Alert,
} from 'react-native';
import {
useForexInvestments,
FOREX_CURRENCY_PAIRS,
computePoolAllocations,
} from '@one_deploy/sdk';
import type { ForexCycleOption } from '@one_deploy/sdk';

function InvestmentForm() {
const { createInvestment, cycleOptions, isLoading } = useForexInvestments();

const [amount, setAmount] = useState('');
const [pair, setPair] = useState(FOREX_CURRENCY_PAIRS[0]);
const [selectedCycle, setSelectedCycle] = useState<ForexCycleOption | null>(null);
const [submitting, setSubmitting] = useState(false);

const deposit = parseFloat(amount) || 0;
const alloc = computePoolAllocations(deposit);

const managementFee = selectedCycle
? deposit * selectedCycle.managementFee
: 0;

const netDeposit = deposit - managementFee;

const handleSubmit = async () => {
if (!selectedCycle || deposit <= 0) return;

if (deposit < selectedCycle.minimumInvestment) {
Alert.alert(
'Below Minimum',
`Minimum investment for ${selectedCycle.label} is $${selectedCycle.minimumInvestment}`
);
return;
}

setSubmitting(true);
try {
const investment = await createInvestment({
amount: deposit,
currencyPair: pair,
cycle: selectedCycle.id,
});

Alert.alert('Success', `Investment ${investment.id} created`);
} catch (err) {
Alert.alert('Error', (err as Error).message);
} finally {
setSubmitting(false);
}
};

if (isLoading) {
return <Text style={styles.loading}>Loading cycle options...</Text>;
}

return (
<ScrollView style={styles.container}>
<Text style={styles.header}>Create Forex Investment</Text>

{/* Amount Input */}
<Text style={styles.label}>Amount (USDC)</Text>
<TextInput
style={styles.input}
value={amount}
onChangeText={setAmount}
placeholder="Enter amount"
placeholderTextColor="#666"
keyboardType="numeric"
/>

{/* Pair Selection */}
<Text style={styles.label}>Currency Pair</Text>
<View style={styles.pairGrid}>
{FOREX_CURRENCY_PAIRS.map((p) => (
<TouchableOpacity
key={p}
style={[styles.pairChip, pair === p && styles.pairChipActive]}
onPress={() => setPair(p)}
>
<Text style={[styles.pairText, pair === p && styles.pairTextActive]}>
{p}
</Text>
</TouchableOpacity>
))}
</View>

{/* Cycle Selection */}
<Text style={styles.label}>Investment Cycle</Text>
{cycleOptions.map((cycle) => (
<TouchableOpacity
key={cycle.id}
style={[
styles.cycleRow,
selectedCycle?.id === cycle.id && styles.cycleRowActive,
!cycle.isAvailable && styles.cycleDisabled,
]}
onPress={() => cycle.isAvailable && setSelectedCycle(cycle)}
disabled={!cycle.isAvailable}
>
<View>
<Text style={styles.cycleLabel}>{cycle.label}</Text>
<Text style={styles.cycleMeta}>
{cycle.durationDays} days | Min ${cycle.minimumInvestment}
</Text>
</View>
<View style={{ alignItems: 'flex-end' }}>
<Text style={styles.cycleFee}>
Mgmt: {(cycle.managementFee * 100).toFixed(1)}%
</Text>
<Text style={styles.cycleFee}>
Perf: {(cycle.performanceFee * 100).toFixed(0)}%
</Text>
</View>
</TouchableOpacity>
))}

{/* Allocation Preview */}
{deposit > 0 && selectedCycle && (
<View style={styles.preview}>
<Text style={styles.previewHeader}>Allocation Preview</Text>
<View style={styles.previewRow}>
<Text style={styles.previewLabel}>Deposit</Text>
<Text style={styles.previewValue}>${deposit.toLocaleString()}</Text>
</View>
<View style={styles.previewRow}>
<Text style={styles.previewLabel}>Management Fee</Text>
<Text style={styles.previewValue}>-${managementFee.toFixed(2)}</Text>
</View>
<View style={styles.previewRow}>
<Text style={styles.previewLabel}>Net Deposit</Text>
<Text style={styles.previewValue}>${netDeposit.toFixed(2)}</Text>
</View>
<View style={styles.previewRow}>
<Text style={styles.previewLabel}>Trading Capital</Text>
<Text style={styles.previewValue}>
${alloc.tradingCapital.toLocaleString()}
</Text>
</View>
<View style={styles.previewRow}>
<Text style={styles.previewLabel}>Pool Reserves</Text>
<Text style={styles.previewValue}>
${alloc.totalReserves.toLocaleString()}
</Text>
</View>
</View>
)}

{/* Submit */}
<TouchableOpacity
style={[styles.submitBtn, (submitting || !selectedCycle) && styles.disabled]}
onPress={handleSubmit}
disabled={submitting || !selectedCycle}
>
<Text style={styles.submitText}>
{submitting ? 'Creating...' : 'Create Investment'}
</Text>
</TouchableOpacity>
</ScrollView>
);
}

const styles = StyleSheet.create({
container: { flex: 1, padding: 16, backgroundColor: '#0d0d1a' },
header: { fontSize: 20, fontWeight: '700', color: '#fff', marginBottom: 20 },
label: { fontSize: 14, fontWeight: '600', color: '#ccc', marginBottom: 8, marginTop: 16 },
input: {
backgroundColor: '#1a1a2e',
color: '#fff',
padding: 12,
borderRadius: 8,
fontSize: 16,
borderWidth: 1,
borderColor: '#333',
},
pairGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
pairChip: {
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 20,
borderWidth: 1,
borderColor: '#444',
},
pairChipActive: { borderColor: '#4488ff', backgroundColor: '#1a2a4e' },
pairText: { color: '#aaa', fontSize: 13 },
pairTextActive: { color: '#4488ff', fontWeight: '600' },
cycleRow: {
flexDirection: 'row',
justifyContent: 'space-between',
padding: 12,
borderRadius: 8,
borderWidth: 1,
borderColor: '#333',
marginBottom: 8,
},
cycleRowActive: { borderColor: '#4488ff', backgroundColor: '#1a2a4e' },
cycleDisabled: { opacity: 0.4 },
cycleLabel: { fontSize: 15, fontWeight: '600', color: '#fff' },
cycleMeta: { fontSize: 12, color: '#888', marginTop: 2 },
cycleFee: { fontSize: 12, color: '#aaa' },
preview: {
backgroundColor: '#1a1a2e',
padding: 16,
borderRadius: 12,
marginTop: 20,
},
previewHeader: { fontSize: 15, fontWeight: '600', color: '#fff', marginBottom: 12 },
previewRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 6 },
previewLabel: { fontSize: 13, color: '#aaa' },
previewValue: { fontSize: 13, color: '#fff', fontFamily: 'monospace' },
submitBtn: {
backgroundColor: '#4488ff',
padding: 16,
borderRadius: 8,
marginTop: 24,
alignItems: 'center',
},
disabled: { opacity: 0.5 },
submitText: { color: '#fff', fontSize: 16, fontWeight: '600' },
loading: { color: '#888', padding: 20, textAlign: 'center' },
});

列出用户投资

import React from 'react';
import { View, Text, FlatList, StyleSheet } from 'react-native';
import { useForexInvestments } from '@one_deploy/sdk';

function InvestmentList() {
const { investments, isLoading } = useForexInvestments();

if (isLoading) return <Text>Loading...</Text>;

return (
<FlatList
data={investments}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.card}>
<View style={styles.cardHeader}>
<Text style={styles.pair}>{item.currencyPair}</Text>
<Text
style={[
styles.status,
item.status === 'active' && { color: '#44cc88' },
item.status === 'completed' && { color: '#4488ff' },
item.status === 'cancelled' && { color: '#cc4444' },
]}
>
{item.status.toUpperCase()}
</Text>
</View>
<Text style={styles.amount}>
${item.amount.toLocaleString()} | Cycle: {item.cycle}
</Text>
<Text style={styles.pnl}>
P&L: {item.pnl >= 0 ? '+' : ''}${item.pnl.toFixed(2)} (
{(item.returnRate * 100).toFixed(2)}%)
</Text>
<Text style={styles.expiry}>
Expires: {new Date(item.expiresAt).toLocaleDateString()}
</Text>
</View>
)}
/>
);
}

const styles = StyleSheet.create({
card: { padding: 14, borderBottomWidth: 1, borderColor: '#2a2a4e' },
cardHeader: { flexDirection: 'row', justifyContent: 'space-between' },
pair: { fontSize: 16, fontWeight: '700', color: '#fff' },
status: { fontSize: 12, fontWeight: '600', color: '#888' },
amount: { fontSize: 13, color: '#aaa', marginTop: 4 },
pnl: { fontSize: 14, color: '#44cc88', marginTop: 4, fontFamily: 'monospace' },
expiry: { fontSize: 12, color: '#666', marginTop: 4 },
});

后续步骤