453 lines
11 KiB
JavaScript
453 lines
11 KiB
JavaScript
import { writeFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
|
|
const root = 'e:/Gitea/ai_trade_assistance'
|
|
|
|
function w(rel, content) {
|
|
const path = join(root, rel)
|
|
writeFileSync(path, content, 'utf8')
|
|
console.log('ok', rel)
|
|
}
|
|
|
|
// Use unicode escapes so this repair script itself stays ASCII-safe on Windows shells.
|
|
const S = {
|
|
mai: '\u4e70',
|
|
mai2: '\u5356',
|
|
shezhi: '\u8bbe\u7f6e',
|
|
quxiao: '\u53d6\u6d88',
|
|
baocun: '\u4fdd\u5b58',
|
|
eci: '\u989d\u5916\u5173\u952e\u8bcd',
|
|
eciPh: '\u7528\u4e8e\u65b0\u95fb\u8fc7\u6ee4\u4e0e\u5206\u6790\u589e\u5f3a\uff0c\u591a\u4e2a\u8bcd\u7528\u9017\u53f7\u6216\u6362\u884c\u5206\u9694',
|
|
saved: '\u8bbe\u7f6e\u5df2\u4fdd\u5b58\u5230\u672c\u5730',
|
|
zongcc: '\u603b\u6301\u4ed3',
|
|
cjl: '\u6210\u4ea4\u91cf',
|
|
jingcc: '\u51c0\u6301\u4ed3',
|
|
placeholder: '\u5f53\u524d\u4e3a\u5360\u4f4d\u6570\u636e\uff08\u7ed3\u6784\u4e0e\u603b\u6301\u4ed3\u4e00\u81f4\uff09',
|
|
long20: '\u591a\u5355\u6301\u4ed3\u524d20\u540d',
|
|
short20: '\u7a7a\u5355\u6301\u4ed3\u524d20\u540d',
|
|
duodan: '\u591a\u5355',
|
|
kongdan: '\u7a7a\u5355',
|
|
mingci: '\u540d\u6b21',
|
|
hyjc: '\u4f1a\u5458\u7b80\u79f0',
|
|
zengjian: '\u589e\u51cf',
|
|
shuliang: '\u6570\u91cf',
|
|
kaipan: '\u5f00\u76d8',
|
|
zhangdie: '\u6da8\u8dcc',
|
|
junjia: '\u5747\u4ef7',
|
|
zuigao: '\u6700\u9ad8',
|
|
jiesuan: '\u7ed3\u7b97',
|
|
cje: '\u6210\u4ea4\u989d',
|
|
yi: '\u4ebf',
|
|
ccl: '\u6301\u4ed3\u91cf',
|
|
zhenfu: '\u632f\u5e45',
|
|
waipan: '\u5916\u76d8',
|
|
zuoshou: '\u6628\u6536',
|
|
neipan: '\u5185\u76d8',
|
|
zuidi: '\u6700\u4f4e',
|
|
zuojie: '\u6628\u7ed3',
|
|
fenshi: '\u5206\u65f6',
|
|
wuri: '\u4e94\u65e5',
|
|
riK: '\u65e5K',
|
|
zhouK: '\u5468K',
|
|
yueK: '\u6708K',
|
|
junjia2: '\u5747\u4ef7',
|
|
xindu: '\u7f6e\u4fe1\u5ea6',
|
|
yijian: '\u4e00\u952e\u83b7\u53d6\u5efa\u8bae',
|
|
shouqi: '\u6536\u8d77',
|
|
zhankai: '\u5c55\u5f00',
|
|
hint: '\u57fa\u4e8e\u884c\u60c5\u3001\u76d8\u53e3\u3001\u65b0\u95fb\u4e0e\u6301\u4ed3\u7684\u7efc\u5408\u5206\u6790\uff08\u5f53\u524d\u4e3a Mock\uff09',
|
|
warn: '\u8bf7\u5148\u5728\u8bbe\u7f6e\u4e2d\u914d\u7f6e DeepSeek API Key\uff08\u672c\u9636\u6bb5\u4ecd\u8fd4\u56de Mock \u5efa\u8bae\uff09',
|
|
qita: '\u5176\u4ed6',
|
|
shuliang2: '\u6570\u91cf',
|
|
zhanbi: '\u5360\u6bd4',
|
|
}
|
|
|
|
w(
|
|
'src/components/quote/OrderBook.vue',
|
|
`<template>
|
|
<div class="order-book card">
|
|
<div class="sentiment">
|
|
<div class="buy" :style="{ flex: quote.buyRatio }">${S.mai} {{ quote.buyRatio }}%</div>
|
|
<div class="sell" :style="{ flex: quote.sellRatio }">${S.mai2} {{ quote.sellRatio }}%</div>
|
|
</div>
|
|
<div class="levels">
|
|
<div
|
|
v-for="ask in quote.orderBook.asks"
|
|
:key="'a' + ask.level"
|
|
class="level ask"
|
|
>
|
|
<span class="side">${S.mai2}{{ ask.level }}</span>
|
|
<span class="price price-down">{{ ask.price.toFixed(2) }}</span>
|
|
<span class="vol">{{ ask.volume }}</span>
|
|
</div>
|
|
<div class="divider" />
|
|
<div v-for="bid in quote.orderBook.bids" :key="'b' + bid.level" class="level bid">
|
|
<span class="side">${S.mai}{{ bid.level }}</span>
|
|
<span class="price price-up">{{ bid.price.toFixed(2) }}</span>
|
|
<span class="vol">{{ bid.volume }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { QuoteData } from '../../types'
|
|
|
|
defineProps<{
|
|
quote: QuoteData
|
|
}>()
|
|
</script>
|
|
|
|
<style scoped>
|
|
.order-book {
|
|
padding: 12px;
|
|
}
|
|
|
|
.sentiment {
|
|
display: flex;
|
|
height: 22px;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
font-size: 12px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.buy,
|
|
.sell {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #fff;
|
|
min-width: 48px;
|
|
}
|
|
|
|
.buy {
|
|
background: var(--up);
|
|
}
|
|
|
|
.sell {
|
|
background: var(--down);
|
|
}
|
|
|
|
.level {
|
|
display: grid;
|
|
grid-template-columns: 40px 1fr 48px;
|
|
font-size: 13px;
|
|
line-height: 1.9;
|
|
}
|
|
|
|
.side {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.price,
|
|
.vol {
|
|
font-variant-numeric: tabular-nums;
|
|
text-align: right;
|
|
}
|
|
|
|
.divider {
|
|
height: 1px;
|
|
background: var(--border);
|
|
margin: 4px 0;
|
|
}
|
|
</style>
|
|
`,
|
|
)
|
|
|
|
w(
|
|
'src/components/header/SettingsDialog.vue',
|
|
`<template>
|
|
<el-dialog
|
|
:model-value="modelValue"
|
|
title="AI ${S.shezhi}"
|
|
width="480px"
|
|
destroy-on-close
|
|
@update:model-value="emit('update:modelValue', $event)"
|
|
>
|
|
<el-form label-position="top">
|
|
<el-form-item label="DeepSeek API Key">
|
|
<el-input
|
|
v-model="localKey"
|
|
type="password"
|
|
show-password
|
|
placeholder="sk-..."
|
|
autocomplete="off"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item label="${S.eci}">
|
|
<el-input
|
|
v-model="localKeywords"
|
|
type="textarea"
|
|
:rows="3"
|
|
placeholder="${S.eciPh}"
|
|
/>
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="emit('update:modelValue', false)">${S.quxiao}</el-button>
|
|
<el-button type="primary" @click="save">${S.baocun}</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, watch } from 'vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import { useSettingsStore } from '../../stores/settings'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
modelValue?: boolean
|
|
}>(),
|
|
{ modelValue: false },
|
|
)
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: boolean]
|
|
}>()
|
|
|
|
const settings = useSettingsStore()
|
|
const localKey = ref('')
|
|
const localKeywords = ref('')
|
|
|
|
watch(
|
|
() => props.modelValue,
|
|
(open) => {
|
|
if (open) {
|
|
localKey.value = settings.apiKey
|
|
localKeywords.value = settings.keywords
|
|
}
|
|
},
|
|
)
|
|
|
|
function save() {
|
|
settings.apiKey = localKey.value.trim()
|
|
settings.keywords = localKeywords.value.trim()
|
|
ElMessage.success('${S.saved}')
|
|
emit('update:modelValue', false)
|
|
}
|
|
</script>
|
|
`,
|
|
)
|
|
|
|
w(
|
|
'src/components/position/PositionPanel.vue',
|
|
`<template>
|
|
<section class="position-panel card">
|
|
<el-tabs v-model="tab">
|
|
<el-tab-pane label="${S.zongcc}" name="total" />
|
|
<el-tab-pane label="${S.cjl}" name="volume" />
|
|
<el-tab-pane label="${S.jingcc}" name="net" />
|
|
</el-tabs>
|
|
<div v-if="tab !== 'total'" class="placeholder-tip">${S.placeholder}</div>
|
|
<div class="cols">
|
|
<div class="col">
|
|
<h3 class="long-title">${S.long20}</h3>
|
|
<PositionDonut :list="data.long" />
|
|
<PositionTable :list="data.long" qty-label="${S.duodan}" />
|
|
</div>
|
|
<div class="col">
|
|
<h3 class="short-title">${S.short20}</h3>
|
|
<PositionDonut :list="data.short" />
|
|
<PositionTable :list="data.short" qty-label="${S.kongdan}" />
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref } from 'vue'
|
|
import type { PositionsData } from '../../types'
|
|
import PositionDonut from './PositionDonut.vue'
|
|
import PositionTable from './PositionTable.vue'
|
|
|
|
defineProps<{
|
|
data: PositionsData
|
|
}>()
|
|
|
|
const tab = ref('total')
|
|
</script>
|
|
|
|
<style scoped>
|
|
.position-panel {
|
|
padding: 12px 16px 16px;
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.placeholder-tip {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
margin: -4px 0 8px;
|
|
}
|
|
|
|
.cols {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 20px;
|
|
}
|
|
|
|
.long-title {
|
|
margin: 0 0 8px;
|
|
color: var(--long);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.short-title {
|
|
margin: 0 0 8px;
|
|
color: var(--short);
|
|
font-size: 14px;
|
|
}
|
|
|
|
@media (max-width: 960px) {
|
|
.cols {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
`,
|
|
)
|
|
|
|
w(
|
|
'src/components/position/PositionTable.vue',
|
|
`<template>
|
|
<el-table :data="list" size="small" stripe class="pos-table" max-height="320">
|
|
<el-table-column prop="rank" label="${S.mingci}" width="56" />
|
|
<el-table-column prop="name" label="${S.hyjc}" min-width="110" />
|
|
<el-table-column :prop="String(qtyKey)" :label="qtyLabel" min-width="90" />
|
|
<el-table-column label="${S.zengjian}" min-width="80">
|
|
<template #default="{ row }">
|
|
<span :class="row.change >= 0 ? 'price-up' : 'chg-down'">
|
|
{{ row.change > 0 ? '+' : '' }}{{ row.change }}
|
|
</span>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { PositionRow } from '../../types'
|
|
|
|
withDefaults(
|
|
defineProps<{
|
|
list: PositionRow[]
|
|
qtyKey?: keyof PositionRow
|
|
qtyLabel?: string
|
|
}>(),
|
|
{
|
|
qtyKey: 'qty',
|
|
qtyLabel: '${S.shuliang}',
|
|
},
|
|
)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.pos-table {
|
|
width: 100%;
|
|
}
|
|
|
|
.chg-down {
|
|
color: var(--short);
|
|
}
|
|
</style>
|
|
`,
|
|
)
|
|
|
|
w(
|
|
'src/components/quote/QuoteStats.vue',
|
|
`<template>
|
|
<div class="quote-stats">
|
|
<div class="col" v-for="(col, i) in columns" :key="i">
|
|
<div class="row" v-for="item in col" :key="item.label">
|
|
<span class="label">{{ item.label }}</span>
|
|
<span class="value" :class="item.className">{{ item.value }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import type { QuoteData } from '../../types'
|
|
|
|
interface StatCell {
|
|
label: string
|
|
value: string
|
|
className?: string
|
|
}
|
|
|
|
const props = defineProps<{
|
|
quote: QuoteData
|
|
}>()
|
|
|
|
const columns = computed((): StatCell[][] => {
|
|
const q = props.quote
|
|
const chgClass = q.change >= 0 ? 'price-up' : 'price-down'
|
|
return [
|
|
[
|
|
{ label: '${S.kaipan}', value: q.open.toFixed(2) },
|
|
{ label: '${S.zhangdie}', value: \`\${q.change.toFixed(2)}\`, className: chgClass },
|
|
{ label: '${S.junjia}', value: q.avg.toFixed(2) },
|
|
],
|
|
[
|
|
{ label: '${S.zuigao}', value: q.high.toFixed(2), className: 'price-up' },
|
|
{ label: '${S.jiesuan}', value: q.settlement.toFixed(2) },
|
|
{ label: '${S.cje}', value: \`\${q.amount}${S.yi}\` },
|
|
],
|
|
[
|
|
{ label: '${S.ccl}', value: q.openInterest.toLocaleString() },
|
|
{ label: '${S.zhenfu}', value: \`\${q.amplitude}%\` },
|
|
{ label: '${S.waipan}', value: q.outerVol.toLocaleString(), className: 'price-up' },
|
|
],
|
|
[
|
|
{ label: '${S.zuoshou}', value: q.prevClose.toFixed(2) },
|
|
{ label: '${S.cjl}', value: q.volume.toLocaleString() },
|
|
{ label: '${S.neipan}', value: q.innerVol.toLocaleString(), className: 'price-down' },
|
|
],
|
|
[
|
|
{ label: '${S.zuidi}', value: q.low.toFixed(2), className: 'price-down' },
|
|
{ label: '${S.zuojie}', value: q.prevSettlement.toFixed(2) },
|
|
],
|
|
]
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.quote-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(5, 1fr);
|
|
gap: 8px 16px;
|
|
padding: 12px 16px;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
font-size: 13px;
|
|
line-height: 1.8;
|
|
}
|
|
|
|
.label {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.value {
|
|
font-variant-numeric: tabular-nums;
|
|
font-weight: 500;
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.quote-stats {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
}
|
|
</style>
|
|
`,
|
|
)
|
|
|
|
console.log('batch1 done')
|