You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
580 lines
27 KiB
580 lines
27 KiB
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>BRAIN Transformer - Web Interface</title>
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
|
<style>
|
|
.simulator-container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
.simulator-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 20px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.simulator-panel {
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
border: 1px solid #dee2e6;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
font-weight: 600;
|
|
color: #495057;
|
|
}
|
|
|
|
.form-group input, .form-group textarea, .form-group select {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 1px solid #ced4da;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.form-group input[type="number"] {
|
|
width: 120px;
|
|
}
|
|
|
|
.log-viewer {
|
|
background: #1e1e1e;
|
|
color: #f8f8f2;
|
|
padding: 15px;
|
|
border-radius: 4px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 12px;
|
|
height: 500px;
|
|
overflow-y: auto;
|
|
white-space: pre-wrap;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.btn-block {
|
|
width: 100%;
|
|
margin-top: 10px;
|
|
display: block;
|
|
}
|
|
|
|
.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.header-flex {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="simulator-container">
|
|
<div class="header-flex">
|
|
<h1>BRAIN Transformer (72变)</h1>
|
|
<a href="/" class="btn btn-outline">Back to Home</a>
|
|
</div>
|
|
|
|
<div class="simulator-grid">
|
|
<div class="simulator-panel">
|
|
<h3>Configuration</h3>
|
|
<form id="transformerForm">
|
|
<div class="form-group">
|
|
<label for="llm_model">LLM Model Name</label>
|
|
<input type="text" id="llm_model" name="LLM_model_name" value="kimi-k2-turbo-preview">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="llm_api_key">LLM API Key</label>
|
|
<input type="password" id="llm_api_key" name="LLM_API_KEY" placeholder="Enter your API Key">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="llm_base_url">LLM Base URL</label>
|
|
<input type="text" id="llm_base_url" name="llm_base_url" value="https://api.moonshot.cn/v1">
|
|
</div>
|
|
<button type="button" id="testConnectionBtn" class="btn btn-secondary btn-block">Test LLM Connection</button>
|
|
<div id="connectionStatus" style="margin-top: 5px; font-size: 0.9em;"></div>
|
|
<hr>
|
|
<div class="form-group">
|
|
<label for="username">BRAIN Username</label>
|
|
<input type="text" id="username" name="username" placeholder="Email or Username">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="password">BRAIN Password</label>
|
|
<input type="password" id="password" name="password">
|
|
</div>
|
|
<button type="button" id="loginBtn" class="btn btn-info btn-block" style="background-color: #17a2b8; color: white;">Login & Fetch Options</button>
|
|
<div id="loginStatus" style="margin-top: 5px; font-size: 0.9em;"></div>
|
|
<hr>
|
|
<div class="form-group">
|
|
<label for="template_summary_content">Template Summary</label>
|
|
<div style="display: flex; gap: 10px; margin-bottom: 5px;">
|
|
<button type="button" class="btn btn-small btn-outline" onclick="document.getElementById('templateFile').click()">📂 Load from File</button>
|
|
<input type="file" id="templateFile" accept=".txt,.md" style="display: none;" onchange="loadTemplateFile(event)">
|
|
</div>
|
|
<textarea id="template_summary_content" name="template_summary_content" style="height: 150px; font-family: monospace; font-size: 12px;" placeholder="Loading default template summary..."></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="alpha_id">Alpha ID</label>
|
|
<input type="text" id="alpha_id" name="alpha_id" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="top_n">Datafield Top N (数值越大变种数量越多,建议低于50)</label>
|
|
<input type="number" id="top_n" name="top_n_datafield" value="50">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="region">目标地区 (可选 - 留空则使用种子Alpha的默认值)</label>
|
|
<select id="region" name="region" disabled>
|
|
<option value="">使用种子Alpha默认值</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="delay">目标Delay (可选 - 留空则使用种子Alpha的默认值)</label>
|
|
<select id="delay" name="delay" disabled>
|
|
<option value="">使用种子Alpha默认值</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="data_type">Data Type</label>
|
|
<select id="data_type" name="data_type">
|
|
<option value="MATRIX" selected>MATRIX</option>
|
|
<option value="VECTOR">VECTOR</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="universe">目标股票池 (可选 - 留空则使用种子Alpha的默认值)</label>
|
|
<select id="universe" name="universe" disabled>
|
|
<option value="">使用种子Alpha默认值</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>目标数据类别 (可多选)</label>
|
|
<div id="category-container" style="max-height: 300px; overflow-y: auto; border: 1px solid #ced4da; padding: 10px; border-radius: 4px; background-color: white;">
|
|
<div id="category-buttons" style="display: flex; flex-wrap: wrap; gap: 8px;">
|
|
<button type="button" class="btn btn-sm btn-primary category-btn active" id="cat_btn_all" data-value="" onclick="toggleCategory(this)">
|
|
不筛选 (默认)
|
|
</button>
|
|
<!-- Dynamic buttons will be added here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" id="runBtn" class="btn btn-primary btn-block" disabled>Run Transformer</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="simulator-panel">
|
|
<h3>Output Log</h3>
|
|
<div id="logViewer" class="log-viewer">Waiting to start...</div>
|
|
<div id="downloadSection" class="hidden" style="margin-top: 20px; text-align: center;">
|
|
<p>Generation Complete!</p>
|
|
<div style="display: flex; flex-direction: column; gap: 10px;">
|
|
<a id="downloadCandidatesBtn" href="#" class="btn btn-success" style="background-color: #28a745; color: white;" download>Download Alpha_candidates.json</a>
|
|
<small style="color: #6c757d;">Alpha 模板文件,可放入<a href="/">首页</a>载入并精细化调整</small>
|
|
|
|
<a id="downloadSuccessBtn" href="#" class="btn btn-info" style="background-color: #17a2b8; color: white;" download>Download Alpha_generated_expressions_success.json</a>
|
|
<small style="color: #6c757d;">Alpha表达式列表,可放入<a href="/">首页</a>或machine_lib载入后添加setting进行回测</small>
|
|
|
|
<a id="downloadErrorBtn" href="#" class="btn btn-warning" style="background-color: #ffc107; color: black;" download>Download Alpha_generated_expressions_error.json</a>
|
|
<small style="color: #6c757d;">被过滤的Alpha表达式,可检查AI模板是否有大规模错误</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top: 30px; padding: 15px; background-color: #e9ecef; border-radius: 5px; font-size: 0.9em; color: #495057;">
|
|
<h4 style="margin-top: 0; color: #343a40;">使用说明</h4>
|
|
<ol style="padding-left: 20px; margin-bottom: 0;">
|
|
<li>请在输入大语言模型相关参数后点击测试,测试成功再进行下一步。</li>
|
|
<li>如遇任何报错,欢迎使用AI帮助你解答,有时候一个截图,AI就能帮你解答了。</li>
|
|
<li>本页中的Template Summary是启发72变的核心提示词,请不断丰富完善您的武林秘籍并替换,你的结果会更好。</li>
|
|
<li>72变支持将原本的Alpha到另外一个区跨区变身,现在它已经从原版本纵向同区点塔,变升级支持跨区点塔。</li>
|
|
<li>生成后的模板配置,您可以到<a href="/">主页</a>进行加载后按需修改,增加个性化内容,这会使得您的Alpha更加独特。</li>
|
|
<li>如果您不想修改生成后的模板,也可以直接载入生成好的表达式列表,入口同样在<a href="/">主页</a>。</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const form = document.getElementById('transformerForm');
|
|
const logViewer = document.getElementById('logViewer');
|
|
const runBtn = document.getElementById('runBtn');
|
|
const testConnectionBtn = document.getElementById('testConnectionBtn');
|
|
const connectionStatus = document.getElementById('connectionStatus');
|
|
const downloadSection = document.getElementById('downloadSection');
|
|
const downloadCandidatesBtn = document.getElementById('downloadCandidatesBtn');
|
|
const downloadSuccessBtn = document.getElementById('downloadSuccessBtn');
|
|
const downloadErrorBtn = document.getElementById('downloadErrorBtn');
|
|
const templateSummaryTextarea = document.getElementById('template_summary_content');
|
|
|
|
const loginBtn = document.getElementById('loginBtn');
|
|
const loginStatus = document.getElementById('loginStatus');
|
|
const regionSelect = document.getElementById('region');
|
|
const delaySelect = document.getElementById('delay');
|
|
const universeSelect = document.getElementById('universe');
|
|
// const categorySelect = document.getElementById('category'); // Removed
|
|
|
|
let optionsData = {};
|
|
|
|
loginBtn.addEventListener('click', async () => {
|
|
const username = document.getElementById('username').value;
|
|
const password = document.getElementById('password').value;
|
|
|
|
if (!username || !password) {
|
|
loginStatus.textContent = 'Please enter Username and Password';
|
|
loginStatus.style.color = 'red';
|
|
return;
|
|
}
|
|
|
|
loginStatus.textContent = 'Logging in and fetching options...';
|
|
loginStatus.style.color = 'blue';
|
|
loginBtn.disabled = true;
|
|
|
|
try {
|
|
const response = await fetch('/api/transformer/login-and-fetch-options', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password })
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
loginStatus.textContent = 'Login Successful! Options fetched.';
|
|
loginStatus.style.color = 'green';
|
|
optionsData = result.options;
|
|
|
|
// Enable Run button
|
|
runBtn.disabled = false;
|
|
|
|
// Populate Region Select
|
|
populateRegionSelect();
|
|
|
|
// Enable selects
|
|
regionSelect.disabled = false;
|
|
|
|
// Populate Category Select
|
|
if (result.categories) {
|
|
populateCategorySelect(result.categories);
|
|
}
|
|
|
|
} else {
|
|
loginStatus.textContent = 'Error: ' + result.error;
|
|
loginStatus.style.color = 'red';
|
|
loginBtn.disabled = false;
|
|
}
|
|
} catch (error) {
|
|
loginStatus.textContent = 'Error: ' + error.message;
|
|
loginStatus.style.color = 'red';
|
|
loginBtn.disabled = false;
|
|
}
|
|
});
|
|
|
|
function populateRegionSelect() {
|
|
// Clear existing options except default
|
|
while (regionSelect.options.length > 1) {
|
|
regionSelect.remove(1);
|
|
}
|
|
|
|
const regions = Object.keys(optionsData);
|
|
regions.forEach(region => {
|
|
const option = document.createElement('option');
|
|
option.value = region;
|
|
option.textContent = region;
|
|
regionSelect.appendChild(option);
|
|
});
|
|
}
|
|
|
|
function populateCategorySelect(categories) {
|
|
const container = document.getElementById('category-buttons');
|
|
// Remove all buttons except the first one (All)
|
|
while (container.children.length > 1) {
|
|
container.removeChild(container.lastChild);
|
|
}
|
|
|
|
categories.forEach(category => {
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'btn btn-sm btn-outline-secondary category-btn';
|
|
|
|
let labelText = '';
|
|
let value = '';
|
|
if (typeof category === 'object' && category !== null) {
|
|
value = category.id;
|
|
labelText = category.name;
|
|
} else {
|
|
value = category;
|
|
labelText = String(category).charAt(0).toUpperCase() + String(category).slice(1);
|
|
}
|
|
|
|
btn.textContent = labelText;
|
|
btn.dataset.value = value;
|
|
btn.onclick = function() { toggleCategory(this); };
|
|
|
|
container.appendChild(btn);
|
|
});
|
|
}
|
|
|
|
function toggleCategory(btn) {
|
|
const allBtn = document.getElementById('cat_btn_all');
|
|
const isAllBtn = (btn === allBtn);
|
|
|
|
if (isAllBtn) {
|
|
// If clicking "All", activate it and deactivate others
|
|
if (!btn.classList.contains('btn-primary')) {
|
|
btn.classList.remove('btn-outline-secondary');
|
|
btn.classList.add('btn-primary');
|
|
btn.classList.add('active');
|
|
|
|
const otherBtns = document.querySelectorAll('.category-btn:not(#cat_btn_all)');
|
|
otherBtns.forEach(b => {
|
|
b.classList.remove('btn-primary');
|
|
b.classList.remove('active');
|
|
b.classList.add('btn-outline-secondary');
|
|
});
|
|
}
|
|
} else {
|
|
// Toggle current button
|
|
if (btn.classList.contains('btn-primary')) {
|
|
// Deactivate
|
|
btn.classList.remove('btn-primary');
|
|
btn.classList.remove('active');
|
|
btn.classList.add('btn-outline-secondary');
|
|
} else {
|
|
// Activate
|
|
btn.classList.remove('btn-outline-secondary');
|
|
btn.classList.add('btn-primary');
|
|
btn.classList.add('active');
|
|
}
|
|
|
|
// Check if any specific category is active
|
|
const anyActive = document.querySelectorAll('.category-btn:not(#cat_btn_all).btn-primary').length > 0;
|
|
|
|
if (anyActive) {
|
|
// Deactivate "All"
|
|
allBtn.classList.remove('btn-primary');
|
|
allBtn.classList.remove('active');
|
|
allBtn.classList.add('btn-outline-secondary');
|
|
} else {
|
|
// Activate "All" if nothing else is selected
|
|
allBtn.classList.remove('btn-outline-secondary');
|
|
allBtn.classList.add('btn-primary');
|
|
allBtn.classList.add('active');
|
|
}
|
|
}
|
|
}
|
|
|
|
regionSelect.addEventListener('change', () => {
|
|
const selectedRegion = regionSelect.value;
|
|
|
|
// Reset Delay and Universe
|
|
delaySelect.innerHTML = '<option value="">使用种子Alpha默认值</option>';
|
|
universeSelect.innerHTML = '<option value="">使用种子Alpha默认值</option>';
|
|
delaySelect.disabled = true;
|
|
universeSelect.disabled = true;
|
|
|
|
if (selectedRegion && optionsData[selectedRegion]) {
|
|
const delays = Object.keys(optionsData[selectedRegion]);
|
|
delays.forEach(delay => {
|
|
const option = document.createElement('option');
|
|
option.value = delay;
|
|
option.textContent = delay;
|
|
delaySelect.appendChild(option);
|
|
});
|
|
delaySelect.disabled = false;
|
|
}
|
|
});
|
|
|
|
delaySelect.addEventListener('change', () => {
|
|
const selectedRegion = regionSelect.value;
|
|
const selectedDelay = delaySelect.value;
|
|
|
|
// Reset Universe
|
|
universeSelect.innerHTML = '<option value="">使用种子Alpha默认值</option>';
|
|
universeSelect.disabled = true;
|
|
|
|
if (selectedRegion && selectedDelay && optionsData[selectedRegion][selectedDelay]) {
|
|
const universes = optionsData[selectedRegion][selectedDelay];
|
|
universes.forEach(universe => {
|
|
const option = document.createElement('option');
|
|
option.value = universe;
|
|
option.textContent = universe;
|
|
universeSelect.appendChild(option);
|
|
});
|
|
universeSelect.disabled = false;
|
|
}
|
|
});
|
|
|
|
const dataTypeSelect = document.getElementById('data_type');
|
|
dataTypeSelect.addEventListener('change', function() {
|
|
if (this.value === 'VECTOR') {
|
|
if (!confirm("请确保您输入的原型Alpha中正确地使用了vector operator,否则极容易造成数据类型错误")) {
|
|
this.value = 'MATRIX';
|
|
}
|
|
}
|
|
});
|
|
|
|
let eventSource = null;
|
|
|
|
// Load default template summary on page load
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
try {
|
|
const response = await fetch('/api/get-default-template-summary');
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
templateSummaryTextarea.value = result.summary;
|
|
} else {
|
|
templateSummaryTextarea.value = "Error loading default template summary: " + result.error;
|
|
}
|
|
} catch (error) {
|
|
templateSummaryTextarea.value = "Error loading default template summary: " + error.message;
|
|
}
|
|
});
|
|
|
|
function loadTemplateFile(event) {
|
|
const file = event.target.files[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
templateSummaryTextarea.value = e.target.result;
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
testConnectionBtn.addEventListener('click', async () => {
|
|
const apiKey = document.getElementById('llm_api_key').value;
|
|
const baseUrl = document.getElementById('llm_base_url').value;
|
|
const model = document.getElementById('llm_model').value;
|
|
|
|
if (!apiKey) {
|
|
connectionStatus.textContent = 'Please enter API Key';
|
|
connectionStatus.style.color = 'red';
|
|
return;
|
|
}
|
|
|
|
connectionStatus.textContent = 'Testing connection...';
|
|
connectionStatus.style.color = 'blue';
|
|
|
|
try {
|
|
const response = await fetch('/api/test-llm-connection', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ apiKey, baseUrl, model })
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
connectionStatus.textContent = 'Connection Successful!';
|
|
connectionStatus.style.color = 'green';
|
|
} else {
|
|
connectionStatus.textContent = 'Connection Failed: ' + result.error;
|
|
connectionStatus.style.color = 'red';
|
|
}
|
|
} catch (error) {
|
|
connectionStatus.textContent = 'Error: ' + error.message;
|
|
connectionStatus.style.color = 'red';
|
|
}
|
|
});
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
if (eventSource) {
|
|
eventSource.close();
|
|
}
|
|
|
|
logViewer.textContent = 'Starting Transformer(此步骤稍久)...\n';
|
|
runBtn.disabled = true;
|
|
downloadSection.classList.add('hidden');
|
|
|
|
const formData = new FormData(form);
|
|
const data = Object.fromEntries(formData.entries());
|
|
|
|
// Handle multiple select for category (Buttons)
|
|
const allBtn = document.getElementById('cat_btn_all');
|
|
let selectedCategories = [];
|
|
|
|
if (!allBtn.classList.contains('btn-primary')) {
|
|
const activeBtns = document.querySelectorAll('.category-btn:not(#cat_btn_all).btn-primary');
|
|
selectedCategories = Array.from(activeBtns).map(btn => btn.dataset.value);
|
|
}
|
|
|
|
if (selectedCategories.length > 0) {
|
|
data.category = selectedCategories;
|
|
} else {
|
|
data.category = null;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/run-transformer-web', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
const taskId = result.taskId;
|
|
startLogStreaming(taskId);
|
|
} else {
|
|
logViewer.textContent += 'Error starting: ' + result.error;
|
|
runBtn.disabled = false;
|
|
}
|
|
} catch (error) {
|
|
logViewer.textContent += 'Error: ' + error.message;
|
|
runBtn.disabled = false;
|
|
}
|
|
});
|
|
|
|
function startLogStreaming(taskId) {
|
|
eventSource = new EventSource(`/api/stream-transformer-logs/${taskId}`);
|
|
|
|
eventSource.onmessage = (event) => {
|
|
const data = JSON.parse(event.data);
|
|
if (data.log) {
|
|
logViewer.textContent += data.log; // + '\n';
|
|
logViewer.scrollTop = logViewer.scrollHeight;
|
|
}
|
|
|
|
if (data.status === 'completed') {
|
|
eventSource.close();
|
|
runBtn.disabled = false;
|
|
logViewer.textContent += '\nProcess Completed Successfully!';
|
|
|
|
// Enable download
|
|
downloadCandidatesBtn.href = `/api/download-transformer-result/${taskId}/candidates`;
|
|
downloadSuccessBtn.href = `/api/download-transformer-result/${taskId}/success`;
|
|
downloadErrorBtn.href = `/api/download-transformer-result/${taskId}/error`;
|
|
downloadSection.classList.remove('hidden');
|
|
} else if (data.status === 'error') {
|
|
eventSource.close();
|
|
runBtn.disabled = false;
|
|
logViewer.textContent += '\nProcess Failed!';
|
|
}
|
|
};
|
|
|
|
eventSource.onerror = () => {
|
|
eventSource.close();
|
|
runBtn.disabled = false;
|
|
logViewer.textContent += '\nConnection lost.';
|
|
};
|
|
}
|
|
</script>
|
|
<script src="{{ url_for('static', filename='usage_widget.js') }}"></script>
|
|
</body>
|
|
</html> |