This commit is contained in:
Flatlogic Bot 2026-02-06 12:09:26 +00:00
parent 6a5fce6db2
commit 65d8cd957f
17 changed files with 673 additions and 171 deletions

View File

@ -1,6 +1,6 @@
from .models import SiteSettings
def site_settings(request):
def project_context(request):
settings = SiteSettings.objects.first()
if not settings:
settings = SiteSettings.objects.create(site_name="BitCrypto")
@ -9,3 +9,6 @@ def site_settings(request):
'project_name': settings.site_name,
'project_description': "全球领先的数字资产交易平台"
}
# Alias for compatibility if needed
site_settings = project_context

View File

@ -0,0 +1,69 @@
# Generated by Django 5.2.7 on 2026-02-06 11:38
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0005_sitesettings'),
]
operations = [
migrations.RemoveField(
model_name='order',
name='close_price',
),
migrations.RemoveField(
model_name='order',
name='entry_price',
),
migrations.RemoveField(
model_name='order',
name='filled_amount',
),
migrations.RemoveField(
model_name='order',
name='profit_loss',
),
migrations.AddField(
model_name='account',
name='frozen_balance',
field=models.DecimalField(decimal_places=8, default=0, max_digits=30, verbose_name='冻结余额 (USDT)'),
),
migrations.AddField(
model_name='order',
name='total_usdt',
field=models.DecimalField(blank=True, decimal_places=8, max_digits=30, null=True, verbose_name='成交额 (USDT)'),
),
migrations.AlterField(
model_name='order',
name='amount',
field=models.DecimalField(decimal_places=8, max_digits=30, verbose_name='数量/手数'),
),
migrations.AlterField(
model_name='order',
name='status',
field=models.CharField(choices=[('PENDING', '等待成交'), ('PARTIALLY_FILLED', '部分成交'), ('FILLED', '已成交'), ('CANCELED', '已撤销')], default='PENDING', max_length=20, verbose_name='状态'),
),
migrations.CreateModel(
name='Position',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('symbol', models.CharField(max_length=20, verbose_name='交易对')),
('side', models.CharField(choices=[('LONG', '做多'), ('SHORT', '做空')], max_length=10, verbose_name='方向')),
('leverage', models.IntegerField(default=20, verbose_name='杠杆')),
('entry_price', models.DecimalField(decimal_places=8, max_digits=30, verbose_name='开仓均价')),
('lots', models.DecimalField(decimal_places=8, max_digits=30, verbose_name='手数')),
('margin', models.DecimalField(decimal_places=8, max_digits=30, verbose_name='占用保证金')),
('is_active', models.BooleanField(default=True, verbose_name='是否持仓')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='时间')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='positions', to='core.account')),
],
options={
'verbose_name': '持仓',
'verbose_name_plural': '持仓管理',
},
),
]

View File

@ -57,6 +57,8 @@ class Account(models.Model):
account_type = models.CharField(max_length=20, choices=ACCOUNT_TYPES, default='SIMULATED', verbose_name=_("账户类型"))
balance = models.DecimalField(max_digits=30, decimal_places=8, default=0, verbose_name=_("可用余额 (USDT)"))
frozen_balance = models.DecimalField(max_digits=30, decimal_places=8, default=0, verbose_name=_("冻结余额 (USDT)"))
credit_score = models.IntegerField(default=80, verbose_name=_("信用分"))
kyc_status = models.CharField(max_length=20, choices=KYC_STATUS, default='UNVERIFIED', verbose_name=_("实名认证状态"))
@ -95,11 +97,10 @@ class Order(models.Model):
TYPE_CHOICES = (('LIMIT', _('限价')), ('MARKET', _('市价')))
TRADE_TYPE_CHOICES = (('SPOT', _('现货')), ('CONTRACT', _('合约')))
STATUS_CHOICES = (
('LIVE', _('进行中')),
('PENDING', _('等待成交')),
('PARTIALLY_FILLED', _('部分成交')),
('FILLED', _('已成交')),
('CANCELED', _('已撤销')),
('CLOSED', _('已平仓')),
)
account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name='orders')
@ -109,15 +110,11 @@ class Order(models.Model):
order_type = models.CharField(max_length=10, choices=TYPE_CHOICES, verbose_name=_("类型"))
price = models.DecimalField(max_digits=30, decimal_places=8, null=True, blank=True, verbose_name=_("委托价格"))
amount = models.DecimalField(max_digits=30, decimal_places=8, verbose_name=_("数量"))
filled_amount = models.DecimalField(max_digits=30, decimal_places=8, default=0, verbose_name=_("已成交数量"))
amount = models.DecimalField(max_digits=30, decimal_places=8, verbose_name=_("数量/手数"))
total_usdt = models.DecimalField(max_digits=30, decimal_places=8, null=True, blank=True, verbose_name=_("成交额 (USDT)"))
leverage = models.IntegerField(default=1, verbose_name=_("杠杆倍数"))
entry_price = models.DecimalField(max_digits=30, decimal_places=8, null=True, blank=True, verbose_name=_("入场价格"))
close_price = models.DecimalField(max_digits=30, decimal_places=8, null=True, blank=True, verbose_name=_("平仓价格"))
profit_loss = models.DecimalField(max_digits=30, decimal_places=8, default=0, verbose_name=_("盈亏"))
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='LIVE', verbose_name=_("状态"))
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING', verbose_name=_("状态"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("创建时间"))
class Meta:
@ -127,6 +124,25 @@ class Order(models.Model):
def __str__(self):
return f"{self.trade_type} {self.side} {self.amount} {self.symbol}"
class Position(models.Model):
SIDE_CHOICES = (('LONG', _('做多')), ('SHORT', _('做空')))
account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name='positions')
symbol = models.CharField(max_length=20, verbose_name=_("交易对"))
side = models.CharField(max_length=10, choices=SIDE_CHOICES, verbose_name=_("方向"))
leverage = models.IntegerField(default=20, verbose_name=_("杠杆"))
entry_price = models.DecimalField(max_digits=30, decimal_places=8, verbose_name=_("开仓均价"))
lots = models.DecimalField(max_digits=30, decimal_places=8, verbose_name=_("手数"))
margin = models.DecimalField(max_digits=30, decimal_places=8, verbose_name=_("占用保证金"))
is_active = models.BooleanField(default=True, verbose_name=_("是否持仓"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("时间"))
class Meta:
verbose_name = _("持仓")
verbose_name_plural = _("持仓管理")
def __str__(self):
return f"{self.account.uid} {self.symbol} {self.side} {self.lots}"
class Transaction(models.Model):
TX_TYPE = (('deposit', _('充值')), ('withdraw', _('提现')))
TX_STATUS = (('pending', _('待处理')), ('completed', _('成功')), ('failed', _('失败')))

View File

@ -100,10 +100,10 @@
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item"><a class="nav-link" href="{% url 'index' %}">首页</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'spot_trade' %}">现货交易</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'contract_trade' %}">合约交易</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'market_center' %}">行情中心</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'core:index' %}">首页</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'core:spot_trade' %}">现货交易</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'core:contract_trade' %}">合约交易</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'core:market_center' %}">行情中心</a></li>
</ul>
<div class="d-flex align-items-center">
{% if user.is_authenticated %}
@ -113,15 +113,15 @@
<span>{{ user.username }}</span>
</a>
<ul class="dropdown-menu dropdown-menu-dark dropdown-menu-end shadow">
<li><a class="dropdown-item" href="{% url 'profile' %}"><i class="bi bi-person-badge me-2"></i>个人中心</a></li>
<li><a class="dropdown-item" href="{% url 'profile' %}"><i class="bi bi-wallet2 me-2"></i>我的资产</a></li>
<li><a class="dropdown-item" href="{% url 'core:profile' %}"><i class="bi bi-person-badge me-2"></i>个人中心</a></li>
<li><a class="dropdown-item" href="{% url 'core:profile' %}"><i class="bi bi-wallet2 me-2"></i>我的资产</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="/login/"><i class="bi bi-box-arrow-right me-2"></i>安全退出</a></li>
</ul>
</div>
{% else %}
<a href="{% url 'login' %}" class="nav-link me-3">登录</a>
<a href="{% url 'register' %}" class="btn btn-primary">立即注册</a>
<a href="{% url 'core:login' %}" class="nav-link me-3">登录</a>
<a href="{% url 'core:register' %}" class="btn btn-primary">立即注册</a>
{% endif %}
</div>
</div>
@ -149,32 +149,32 @@
<div class="col-md-2 offset-md-1">
<h6>产品服务</h6>
<ul class="list-unstyled">
<li><a href="{% url 'spot_trade' %}">现货交易</a></li>
<li><a href="{% url 'contract_trade' %}">永续合约</a></li>
<li><a href="{% url 'placeholder' '币安赚币' %}">理财中心</a></li>
<li><a href="{% url 'core:spot_trade' %}">现货交易</a></li>
<li><a href="{% url 'core:contract_trade' %}">永续合约</a></li>
<li><a href="{% url 'core:placeholder' '币安赚币' %}">理财中心</a></li>
</ul>
</div>
<div class="col-md-2">
<h6>法律法规</h6>
<ul class="list-unstyled">
<li><a href="{% url 'placeholder' '服务条款' %}">服务条款</a></li>
<li><a href="{% url 'placeholder' '隐私政策' %}">隐私政策</a></li>
<li><a href="{% url 'placeholder' '免责声明' %}">免责声明</a></li>
<li><a href="{% url 'core:placeholder' '服务条款' %}">服务条款</a></li>
<li><a href="{% url 'core:placeholder' '隐私政策' %}">隐私政策</a></li>
<li><a href="{% url 'core:placeholder' '免责声明' %}">免责声明</a></li>
</ul>
</div>
<div class="col-md-2">
<h6>客户支持</h6>
<ul class="list-unstyled">
<li><a href="{% url 'placeholder' '帮助中心' %}">帮助中心</a></li>
<li><a href="{% url 'placeholder' '公告中心' %}">公告中心</a></li>
<li><a href="{% url 'placeholder' '提交请求' %}">提交工单</a></li>
<li><a href="{% url 'core:placeholder' '帮助中心' %}">帮助中心</a></li>
<li><a href="{% url 'core:placeholder' '公告中心' %}">公告中心</a></li>
<li><a href="{% url 'core:placeholder' '提交请求' %}">提交工单</a></li>
</ul>
</div>
<div class="col-md-2">
<h6>关于我们</h6>
<ul class="list-unstyled">
<li><a href="{% url 'placeholder' '公司简介' %}">公司简介</a></li>
<li><a href="{% url 'placeholder' '加入我们' %}">招贤纳士</a></li>
<li><a href="{% url 'core:placeholder' '公司简介' %}">公司简介</a></li>
<li><a href="{% url 'core:placeholder' '加入我们' %}">招贤纳士</a></li>
</ul>
</div>
</div>

View File

@ -18,8 +18,8 @@
<h1 class="display-3 fw-bold mb-4">开启您的<br><span style="color: var(--accent-color);">加密货币</span>之旅</h1>
<p class="lead text-light mb-5">在全球最受信任的交易平台买卖和存储加密货币。BitCrypto 为您提供安全、稳定、高效的服务。</p>
<div class="d-flex gap-3">
<a href="{% url 'register' %}" class="btn btn-warning btn-lg px-5 fw-bold">立即注册</a>
<a href="{% url 'spot_trade' %}" class="btn btn-outline-light btn-lg px-5 fw-bold">开始交易</a>
<a href="{% url 'core:register' %}" class="btn btn-warning btn-lg px-5 fw-bold">立即注册</a>
<a href="{% url 'core:spot_trade' %}" class="btn btn-outline-light btn-lg px-5 fw-bold">开始交易</a>
</div>
</div>
</div>
@ -32,7 +32,7 @@
<div class="col-lg-7 text-lg-end">
<h1 class="display-4 fw-bold mb-4">领先的<span style="color: var(--accent-color);">衍生品</span>交易平台</h1>
<p class="lead text-light mb-5">最高200倍杠杆支持多种永续合约毫秒级撮合引擎。多空双向交易灵活捕捉市场机会。</p>
<a href="{% url 'contract_trade' %}" class="btn btn-warning btn-lg px-5 fw-bold">进入合约交易</a>
<a href="{% url 'core:contract_trade' %}" class="btn btn-warning btn-lg px-5 fw-bold">进入合约交易</a>
</div>
</div>
</div>
@ -98,7 +98,7 @@
<h2 class="fw-bold">热门行情</h2>
<p class="text-secondary mb-0">实时获取全球顶级加密货币价格走势</p>
</div>
<a href="{% url 'market_center' %}" class="text-warning text-decoration-none fw-bold">查看更多市场 <i class="bi bi-arrow-right"></i></a>
<a href="{% url 'core:market_center' %}" class="text-warning text-decoration-none fw-bold">查看更多市场 <i class="bi bi-arrow-right"></i></a>
</div>
<div class="table-responsive">

View File

@ -41,7 +41,7 @@
</form>
<div class="text-center">
<p class="text-secondary small">还没有账户? <a href="{% url 'register' %}" class="text-warning text-decoration-none fw-bold">立即注册</a></p>
<p class="text-secondary small">还没有账户? <a href="{% url 'core:register' %}" class="text-warning text-decoration-none fw-bold">立即注册</a></p>
</div>
<div class="mt-5 pt-4 border-top border-secondary text-center">

View File

@ -0,0 +1,107 @@
{% extends 'base.html' %}
{% load static %}
{% block content %}
<div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold text-white mb-0">行情中心</h2>
<div class="input-group w-auto">
<span class="input-group-text bg-dark border-secondary"><i class="bi bi-search text-secondary"></i></span>
<input type="text" id="market-search" class="form-control bg-dark text-white border-secondary" placeholder="搜索币种" onkeyup="filterMarket()">
</div>
</div>
<div class="glass-card overflow-hidden" style="border-radius: 12px; border: 1px solid #2b3139; background: #161a1e;">
<div class="table-responsive">
<table class="table table-dark table-hover mb-0 align-middle">
<thead>
<tr class="text-secondary border-bottom border-secondary">
<th class="ps-4 py-3">币种</th>
<th class="py-3">最新价</th>
<th class="py-3">24h 涨跌</th>
<th class="py-3">24h 最高</th>
<th class="py-3">24h 最低</th>
<th class="py-3">24h 成交额</th>
<th class="pe-4 py-3 text-end">操作</th>
</tr>
</thead>
<tbody id="market-table-body">
{% for crypto in cryptos %}
<tr class="market-row" data-symbol="{{ crypto.symbol }}">
<td class="ps-4 py-3">
<div class="d-flex align-items-center">
<img src="https://static.okx.com/cdn/oksupport/asset/currency/icon/{{ crypto.symbol|lower }}.png"
class="rounded-circle me-3" width="32" height="32"
onerror="this.src='https://static.okx.com/cdn/oksupport/asset/currency/icon/generic.png'">
<div>
<div class="fw-bold text-white">{{ crypto.symbol }}</div>
<div class="text-secondary small">{{ crypto.name }}</div>
</div>
</div>
</td>
<td class="py-3 fw-bold price-val">--</td>
<td class="py-3 change-val">--</td>
<td class="py-3 high-val text-secondary">--</td>
<td class="py-3 low-val text-secondary">--</td>
<td class="py-3 vol-val text-secondary">--</td>
<td class="pe-4 py-3 text-end">
<a href="{% url 'core:trade' 'spot' %}?symbol={{ crypto.symbol }}USDT" class="btn btn-outline-warning btn-sm px-3 me-2">现货</a>
<a href="{% url 'core:trade' 'contract' %}?symbol={{ crypto.symbol }}USDT" class="btn btn-warning btn-sm px-3 text-dark">合约</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<style>
.glass-card { background: rgba(22, 26, 30, 0.8); backdrop-filter: blur(10px); }
.table-dark { --bs-table-bg: transparent; }
.table-hover tbody tr:hover { background-color: rgba(255, 255, 255, 0.05) !important; }
</style>
{% endblock %}
{% block scripts %}
<script>
async function updateMarket() {
try {
const r = await fetch('https://api.binance.com/api/v3/ticker/24hr');
const data = await r.json();
const tickers = {};
data.forEach(t => {
if (t.symbol.endsWith('USDT')) {
tickers[t.symbol.replace('USDT', '')] = t;
}
});
document.querySelectorAll('.market-row').forEach(row => {
const sym = row.getAttribute('data-symbol');
const t = tickers[sym];
if (t) {
const chg = parseFloat(t.priceChangePercent);
row.querySelector('.price-val').textContent = parseFloat(t.lastPrice).toLocaleString();
row.querySelector('.price-val').className = 'py-3 fw-bold price-val ' + (chg >= 0 ? 'text-success' : 'text-danger');
row.querySelector('.change-val').textContent = (chg >= 0 ? '+' : '') + chg.toFixed(2) + '%';
row.querySelector('.change-val').className = 'py-3 change-val ' + (chg >= 0 ? 'text-success' : 'text-danger');
row.querySelector('.high-val').textContent = parseFloat(t.highPrice).toLocaleString();
row.querySelector('.low-val').textContent = parseFloat(t.lowPrice).toLocaleString();
row.querySelector('.vol-val').textContent = (parseFloat(t.quoteVolume) / 1000000).toFixed(2) + 'M';
}
});
} catch(e) {}
}
function filterMarket() {
const query = document.getElementById('market-search').value.toLowerCase();
document.querySelectorAll('.market-row').forEach(row => {
const sym = row.getAttribute('data-symbol').toLowerCase();
row.style.display = sym.includes(query) ? '' : 'none';
});
}
updateMarket();
setInterval(updateMarket, 3000);
</script>
{% endblock %}

View File

@ -23,8 +23,8 @@
</div>
<div class="col-lg-auto mt-3 mt-lg-0">
<div class="d-flex gap-2">
<a href="{% url 'deposit' %}" class="btn btn-warning fw-bold px-4">充币</a>
<a href="{% url 'withdraw' %}" class="btn btn-outline-light px-4">提币</a>
<a href="{% url 'core:deposit' %}" class="btn btn-warning fw-bold px-4">充币</a>
<a href="{% url 'core:withdraw' %}" class="btn btn-outline-light px-4">提币</a>
</div>
</div>
</div>
@ -122,7 +122,7 @@
<div class="fw-bold text-white">身份认证 (KYC)</div>
<p class="text-secondary x-small mb-0">提升提现额度至 100 BTC</p>
</div>
<a href="{% url 'verify' %}" class="btn btn-link btn-sm text-warning ms-auto text-decoration-none">立即认证</a>
<a href="{% url 'core:verify' %}" class="btn btn-link btn-sm text-warning ms-auto text-decoration-none">立即认证</a>
</div>
</div>
</div>

View File

@ -66,7 +66,7 @@
<div class="form-check">
<input class="form-check-input bg-transparent border-secondary" type="checkbox" id="terms" required checked>
<label class="form-check-label text-secondary small" for="terms">
我已阅读并同意 <a href="{% url 'placeholder' '服务条款' %}" class="text-warning">服务条款</a><a href="{% url 'placeholder' '隐私政策' %}" class="text-warning">隐私政策</a>
我已阅读并同意 <a href="{% url 'core:placeholder' '服务条款' %}" class="text-warning">服务条款</a><a href="{% url 'core:placeholder' '隐私政策' %}" class="text-warning">隐私政策</a>
</label>
</div>
</div>
@ -75,7 +75,7 @@
</form>
<div class="text-center">
<p class="text-secondary small">已有账户? <a href="{% url 'login' %}" class="text-warning text-decoration-none fw-bold">立即登录</a></p>
<p class="text-secondary small">已有账户? <a href="{% url 'core:login' %}" class="text-warning text-decoration-none fw-bold">立即登录</a></p>
</div>
</div>
</div>

View File

@ -57,8 +57,8 @@
</div>
</div>
<div class="d-flex gap-1 bg-dark p-1" style="border-radius: 4px;">
<a href="{% url 'trade' 'spot' %}?symbol={{ symbol }}" class="btn btn-sm {% if trade_type == 'SPOT' %}btn-warning text-dark{% else %}btn-transparent text-secondary{% endif %} fw-bold px-3">现货</a>
<a href="{% url 'trade' 'contract' %}?symbol={{ symbol }}" class="btn btn-sm {% if trade_type == 'CONTRACT' %}btn-warning text-dark{% else %}btn-transparent text-secondary{% endif %} fw-bold px-3">合约</a>
<a href="{% url 'core:trade' 'spot' %}?symbol={{ symbol }}" class="btn btn-sm {% if trade_type == 'SPOT' %}btn-warning text-dark{% else %}btn-transparent text-secondary{% endif %} fw-bold px-3">现货</a>
<a href="{% url 'core:trade' 'contract' %}?symbol={{ symbol }}" class="btn btn-sm {% if trade_type == 'CONTRACT' %}btn-warning text-dark{% else %}btn-transparent text-secondary{% endif %} fw-bold px-3">合约</a>
</div>
</div>
@ -68,7 +68,7 @@
</div>
<!-- Trading Forms -->
<div class="glass-card p-3" style="border-radius: 4px; border: 1px solid #2b3139; background: #161a1e;">
<div class="glass-card p-3 mb-2" style="border-radius: 4px; border: 1px solid #2b3139; background: #161a1e;">
{% if trade_type == 'SPOT' %}
<div class="row g-4">
<!-- Buy -->
@ -87,9 +87,9 @@
</div>
<div class="input-group input-group-sm mb-3">
<span class="input-group-text bg-dark text-secondary border-secondary" style="width: 70px;">成交额</span>
<input type="number" id="buy-total" class="form-control bg-transparent text-white border-secondary" placeholder="0.00" oninput="updateSlider('buy')">
<span class="input-group-text bg-dark text-secondary border-secondary">USDT</span>
<span class="input-group-text bg-dark text-secondary border-secondary" style="width: 70px;" id="buy-label">数量</span>
<input type="number" id="buy-amount" class="form-control bg-transparent text-white border-secondary" placeholder="0.00" oninput="updateSlider('buy')">
<span class="input-group-text bg-dark text-secondary border-secondary" id="buy-unit">{{ base_symbol }}</span>
</div>
<div class="mb-4 px-1">
@ -103,7 +103,7 @@
<span class="text-secondary">可用余额</span>
<span class="text-white">{{ account.balance|default:"0.00" }} USDT</span>
</div>
<button class="btn btn-success w-100 fw-bold py-2 shadow" onclick="submitOrder('BUY')">买入 <span class="base-asset">{{ symbol|slice:":-4" }}</span></button>
<button class="btn btn-success w-100 fw-bold py-2 shadow" onclick="submitOrder('BUY')">买入 <span class="base-asset">{{ base_symbol }}</span></button>
</div>
<!-- Sell -->
@ -124,7 +124,7 @@
<div class="input-group input-group-sm mb-3">
<span class="input-group-text bg-dark text-secondary border-secondary" style="width: 70px;">数量</span>
<input type="number" id="sell-amount" class="form-control bg-transparent text-white border-secondary" placeholder="0.00" oninput="updateSlider('sell')">
<span class="input-group-text bg-dark text-secondary border-secondary">{{ symbol|slice:":-4" }}</span>
<span class="input-group-text bg-dark text-secondary border-secondary">{{ base_symbol }}</span>
</div>
<div class="mb-4 px-1">
@ -135,10 +135,10 @@
</div>
<div class="d-flex justify-content-between x-small mb-3">
<span class="text-secondary">可用 <span class="base-asset">{{ symbol|slice:":-4" }}</span></span>
<span class="text-white" id="sell-available">0.00</span>
<span class="text-secondary">可用 <span class="base-asset">{{ base_symbol }}</span></span>
<span class="text-white" id="sell-available-display">{{ base_asset_balance|default:"0.00" }} {{ base_symbol }}</span>
</div>
<button class="btn btn-danger w-100 fw-bold py-2 shadow" onclick="submitOrder('SELL')">卖出 <span class="base-asset">{{ symbol|slice:":-4" }}</span></button>
<button class="btn btn-danger w-100 fw-bold py-2 shadow" onclick="submitOrder('SELL')">卖出 <span class="base-asset">{{ base_symbol }}</span></button>
</div>
</div>
{% else %}
@ -147,9 +147,9 @@
<div class="col-12 mb-2 d-flex gap-3 align-items-center">
<div class="btn-group" role="group">
<input type="radio" class="btn-check" name="c-mode" id="c-limit" checked onchange="toggleMode('contract', 'limit')">
<label class="btn btn-outline-warning btn-sm px-4" for="c-limit">限价</label>
<label class="btn btn-outline-warning btn-sm px-4" for="c-limit">限价委托</label>
<input type="radio" class="btn-check" name="c-mode" id="c-market" onchange="toggleMode('contract', 'market')">
<label class="btn btn-outline-warning btn-sm px-4" for="c-market">市价</label>
<label class="btn btn-outline-warning btn-sm px-4" for="c-market">市价委托</label>
</div>
<div class="dropdown">
<button class="btn btn-dark btn-sm border-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" id="lev-btn">杠杆: 20x</button>
@ -169,32 +169,96 @@
</div>
<div class="input-group input-group-sm mb-3">
<span class="input-group-text bg-dark text-secondary border-secondary" style="width: 70px;">手数</span>
<input type="number" id="c-amount" class="form-control bg-transparent text-white border-secondary" placeholder="0">
<input type="number" id="c-amount" class="form-control bg-transparent text-white border-secondary" placeholder="0" oninput="updateSlider('contract')">
<span class="input-group-text bg-dark text-secondary border-secondary"></span>
</div>
<div class="mb-3">
<div class="mb-4 px-1">
<input type="range" class="form-range custom-range" id="c-slider" min="0" max="100" step="1" value="0" oninput="applySlider('contract')">
<div class="d-flex justify-content-between x-small text-secondary mt-1">
<span>0%</span><span>25%</span><span>50%</span><span>75%</span><span>100%</span>
</div>
</div>
<div class="d-flex justify-content-between x-small mb-3 text-secondary">
<span>保证金: <span class="text-white" id="c-margin">0.00</span> USDT</span>
<span>所需保证金: <span class="text-white" id="c-margin-display">0.00</span> USDT</span>
<span>可用: <span class="text-white">{{ account.balance|default:"0.00" }} USDT</span></span>
</div>
<div class="row g-2">
<div class="col-6"><button class="btn btn-success w-100 fw-bold" onclick="submitOrder('BUY')">买入 (做多)</button></div>
<div class="col-6"><button class="btn btn-danger w-100 fw-bold" onclick="submitOrder('SELL')">卖出 (做空)</button></div>
<div class="col-6"><button class="btn btn-success w-100 fw-bold shadow" onclick="submitOrder('BUY')">买入 (做多)</button></div>
<div class="col-6"><button class="btn btn-danger w-100 fw-bold shadow" onclick="submitOrder('SELL')">卖出 (做空)</button></div>
</div>
</div>
<div class="col-md-6">
<div class="p-3 bg-dark rounded border border-secondary" style="font-size: 12px;">
<div class="d-flex justify-content-between mb-2"><span>合约面值</span><span class="text-white">100 USDT</span></div>
<div class="d-flex justify-content-between mb-2"><span>合约面值</span><span class="text-white">100 USDT / 张</span></div>
<div class="d-flex justify-content-between mb-2"><span>当前杠杆</span><span class="text-warning" id="lev-val">20x</span></div>
<div class="d-flex justify-content-between mb-2"><span>预计手续费</span><span class="text-white">0.05%</span></div>
<div class="d-flex justify-content-between"><span>维持保证金</span><span class="text-white">0.4%</span></div>
<div class="d-flex justify-content-between"><span>维持保证金</span><span class="text-white">0.4%</span></div>
</div>
</div>
</div>
{% endif %}
</div>
<!-- Positions / Orders Tabs -->
<div class="glass-card p-3" style="border-radius: 4px; border: 1px solid #2b3139; background: #161a1e;">
<ul class="nav nav-tabs border-0 mb-3" id="tradeTabs" role="tablist">
<li class="nav-item">
<button class="nav-link active bg-transparent text-white border-0 fw-bold border-bottom border-warning border-3" id="positions-tab" data-bs-toggle="tab" data-bs-target="#positions" type="button">当前持仓/订单</button>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="positions">
<div class="table-responsive">
<table class="table table-dark table-sm align-middle mb-0" style="font-size: 12px;">
<thead>
<tr class="text-secondary">
<th>合约/现货</th>
<th>方向</th>
<th>数量/手数</th>
<th>开仓/委托价</th>
<th>当前价</th>
<th>保证金</th>
<th>盈亏</th>
<th class="text-end">操作</th>
</tr>
</thead>
<tbody>
{% if trade_type == 'CONTRACT' %}
{% for p in account.positions.all %}
{% if p.is_active %}
<tr>
<td class="fw-bold">{{ p.symbol }} {{ p.leverage }}x</td>
<td class="{% if p.side == 'LONG' %}text-success{% else %}text-danger{% endif %}">{{ p.get_side_display }}</td>
<td>{{ p.lots }}</td>
<td>{{ p.entry_price }}</td>
<td class="current-p-val" data-symbol="{{ p.symbol }}">--</td>
<td>{{ p.margin|floatformat:2 }}</td>
<td class="upl-val" data-entry="{{ p.entry_price }}" data-side="{{ p.side }}" data-lots="{{ p.lots }}">--</td>
<td class="text-end"><button class="btn btn-outline-danger btn-sm py-0" onclick="closePos({{ p.id }})">平仓</button></td>
</tr>
{% endif %}
{% endfor %}
{% endif %}
{% for o in account.orders.all %}
{% if o.status == 'PENDING' %}
<tr>
<td>{{ o.symbol }} ({{ o.get_trade_type_display }})</td>
<td class="{% if o.side == 'BUY' %}text-success{% else %}text-danger{% endif %}">{{ o.get_side_display }}</td>
<td>{{ o.amount }}</td>
<td>{{ o.price|default:"市价" }}</td>
<td class="current-p-val" data-symbol="{{ o.symbol }}">--</td>
<td>--</td>
<td>等待成交</td>
<td class="text-end"><button class="btn btn-outline-secondary btn-sm py-0">撤单</button></td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Right: Order Book -->
@ -202,7 +266,7 @@
<div class="glass-card h-100 p-2" style="border-radius: 4px; border: 1px solid #2b3139; background: #161a1e;">
<div class="d-flex justify-content-between text-secondary x-small mb-2 px-2">
<span>价格(USDT)</span>
<span>数量({{ symbol|slice:":-4" }})</span>
<span>数量({{ base_symbol }})</span>
</div>
<div id="asks" class="mb-2"></div>
<div class="py-2 text-center border-top border-bottom border-secondary my-2">
@ -216,7 +280,7 @@
<style>
.x-small { font-size: 11px; }
.coin-icon-small { width: 18px; height: 18px; margin-right: 6px; }
.coin-icon-small { width: 18px; height: 18px; margin-right: 8px; vertical-align: middle; border-radius: 50%; }
.order-book-row { display: flex; justify-content: space-between; font-size: 11px; padding: 2px 4px; position: relative; }
.order-book-row span { position: relative; z-index: 1; }
.ask-bg { position: absolute; right: 0; top: 0; bottom: 0; background: rgba(246, 70, 93, 0.15); transition: width 0.3s; }
@ -225,6 +289,7 @@
.custom-range-danger::-webkit-slider-thumb { background: #f6465d; cursor: pointer; }
.coin-list-container::-webkit-scrollbar { width: 4px; }
.coin-list-container::-webkit-scrollbar-thumb { background: #2b3139; border-radius: 2px; }
.nav-tabs .nav-link:hover { color: #fcd535 !important; }
</style>
{% endblock %}
@ -234,6 +299,9 @@
const symbol = '{{ symbol }}';
const tradeType = '{{ trade_type }}';
const balance = parseFloat("{{ account.balance|default:0 }}");
const baseAsset = '{{ base_symbol }}';
const assetBalance = parseFloat("{{ base_asset_balance|default:0 }}");
let currentPrice = 0;
let leverage = 20;
let allCoins = [];
@ -250,7 +318,7 @@
try {
const r = await fetch('https://api.binance.com/api/v3/ticker/24hr');
const data = await r.json();
allCoins = data.filter(c => c.symbol.endsWith('USDT')).slice(0, 50);
allCoins = data.filter(c => c.symbol.endsWith('USDT')).slice(0, 100);
renderCoins();
} catch(e) {}
}
@ -264,14 +332,15 @@
if (filter && !c.symbol.toLowerCase().includes(filter.toLowerCase())) return;
const base = c.symbol.replace('USDT', '');
const chg = parseFloat(c.priceChangePercent);
const iconUrl = `https://static.okx.com/cdn/oksupport/asset/currency/icon/${base.toLowerCase()}.png`;
const row = `
<tr style="cursor: pointer;" onclick="location.href='?symbol=${c.symbol}'">
<td>${base}</td>
<td class="text-end fw-bold">${parseFloat(c.lastPrice).toFixed(2)}</td>
<td><img src="${iconUrl}" class="coin-icon-small" onerror="this.src='https://static.okx.com/cdn/oksupport/asset/currency/icon/generic.png'">${base}</td>
<td class="text-end fw-bold">${parseFloat(c.lastPrice).toLocaleString()}</td>
<td class="text-end ${chg>=0?'text-success':'text-danger'}">${chg>=0?'+':''}${chg.toFixed(2)}%</td>
</tr>`;
list.innerHTML += row;
mobileList.innerHTML += `<li><a class="dropdown-item d-flex justify-content-between" href="?symbol=${c.symbol}"><span>${base}</span><span class="${chg>=0?'text-success':'text-danger'}">${parseFloat(c.lastPrice).toFixed(2)}</span></a></li>`;
mobileList.innerHTML += `<li><a class="dropdown-item d-flex justify-content-between" href="?symbol=${c.symbol}"><span><img src="${iconUrl}" class="coin-icon-small" onerror="this.src='https://static.okx.com/cdn/oksupport/asset/currency/icon/generic.png'">${base}</span><span class="${chg>=0?'text-success':'text-danger'}">${parseFloat(c.lastPrice).toLocaleString()}</span></a></li>`;
});
}
@ -280,7 +349,36 @@
}
function toggleMode(side, mode) {
const pInput = document.getElementById(side === 'contract' ? 'c-price' : side + '-price');
if (side === 'buy') {
const pInput = document.getElementById('buy-price');
const label = document.getElementById('buy-label');
const unit = document.getElementById('buy-unit');
if (mode === 'market') {
pInput.value = '市价价格';
pInput.disabled = true;
pInput.style.color = '#f0b90b';
label.textContent = '成交额';
unit.textContent = 'USDT';
} else {
pInput.value = currentPrice || '';
pInput.disabled = false;
pInput.style.color = 'white';
label.textContent = '数量';
unit.textContent = baseAsset;
}
} else if (side === 'sell') {
const pInput = document.getElementById('sell-price');
if (mode === 'market') {
pInput.value = '市价价格';
pInput.disabled = true;
pInput.style.color = '#f0b90b';
} else {
pInput.value = currentPrice || '';
pInput.disabled = false;
pInput.style.color = 'white';
}
} else if (side === 'contract') {
const pInput = document.getElementById('c-price');
if (mode === 'market') {
pInput.value = '市价价格';
pInput.disabled = true;
@ -291,21 +389,50 @@
pInput.style.color = 'white';
}
}
}
function applySlider(side) {
const val = document.getElementById(side + '-slider').value;
if (side === 'buy') {
document.getElementById('buy-total').value = (balance * (val / 100)).toFixed(2);
const mode = document.getElementById('buy-limit').checked ? 'limit' : 'market';
if (mode === 'market') {
document.getElementById('buy-amount').value = (balance * (val / 100)).toFixed(2);
} else {
const price = parseFloat(document.getElementById('buy-price').value);
if (price > 0) document.getElementById('buy-amount').value = (balance * (val / 100) / price).toFixed(4);
}
} else if (side === 'sell') {
// Simulated 1.5 BTC for testing
document.getElementById('sell-amount').value = (1.5 * (val / 100)).toFixed(4);
document.getElementById('sell-amount').value = (assetBalance * (val / 100)).toFixed(4);
} else if (side === 'contract') {
const margin = (balance * (val / 100));
document.getElementById('c-margin').textContent = margin.toFixed(2);
document.getElementById('c-margin-display').textContent = margin.toFixed(2);
document.getElementById('c-amount').value = Math.floor((margin * leverage) / 100);
}
}
function updateSlider(side) {
if (side === 'buy') {
const mode = document.getElementById('buy-limit').checked ? 'limit' : 'market';
const amount = parseFloat(document.getElementById('buy-amount').value) || 0;
let percent = 0;
if (mode === 'market') {
percent = (amount / balance) * 100;
} else {
const price = parseFloat(document.getElementById('buy-price').value) || 0;
percent = (amount * price / balance) * 100;
}
document.getElementById('buy-slider').value = Math.min(100, percent);
} else if (side === 'sell') {
const amount = parseFloat(document.getElementById('sell-amount').value) || 0;
document.getElementById('sell-slider').value = Math.min(100, (amount / assetBalance) * 100);
} else if (side === 'contract') {
const lots = parseFloat(document.getElementById('c-amount').value) || 0;
const margin = (100 * lots) / leverage;
document.getElementById('c-margin-display').textContent = margin.toFixed(2);
document.getElementById('c-slider').value = Math.min(100, (margin / balance) * 100);
}
}
function setLev(l) {
leverage = l;
document.getElementById('lev-btn').textContent = '杠杆: ' + l + 'x';
@ -328,7 +455,6 @@
document.getElementById('header-change').className = 'fw-bold ' + (chg>=0?'text-success':'text-danger');
document.getElementById('header-volume').textContent = (parseFloat(d.quoteVolume)/1000000).toFixed(2) + 'M';
// Order Book
const askD = document.getElementById('asks'); const bidD = document.getElementById('bids');
askD.innerHTML = ''; bidD.innerHTML = '';
for (let i = 0; i < 10; i++) {
@ -336,18 +462,72 @@
bidD.innerHTML += `<div class="order-book-row"><span class="text-success">${(currentPrice-(i+1)*0.1).toFixed(2)}</span><span>${(Math.random()*1.2).toFixed(4)}</span><div class="bid-bg" style="width:${Math.random()*70}%"></div></div>`;
}
// Sync limit inputs if empty
if (tradeType === 'SPOT') {
if (!document.getElementById('buy-price').value && !document.getElementById('buy-price').disabled) document.getElementById('buy-price').value = currentPrice;
if (!document.getElementById('sell-price').value && !document.getElementById('sell-price').disabled) document.getElementById('sell-price').value = currentPrice;
} else {
if (!document.getElementById('c-price').value && !document.getElementById('c-price').disabled) document.getElementById('c-price').value = currentPrice;
}
document.querySelectorAll('.current-p-val').forEach(el => { el.textContent = currentPrice.toLocaleString(); });
document.querySelectorAll('.upl-val').forEach(el => {
const entry = parseFloat(el.getAttribute('data-entry'));
const side = el.getAttribute('data-side');
const lots = parseFloat(el.getAttribute('data-lots'));
let upl = 0;
if (side === 'LONG') upl = (currentPrice - entry) / entry * (lots * 100);
else upl = (entry - currentPrice) / entry * (lots * 100);
el.textContent = (upl >= 0 ? '+' : '') + upl.toFixed(2) + ' USDT';
el.className = 'upl-val ' + (upl >= 0 ? 'text-success' : 'text-danger');
});
} catch(e) {}
}
function submitOrder(side) {
alert('订单已提交,正在匹配中...');
async function submitOrder(side) {
const mode = tradeType === 'SPOT'
? (document.getElementById(side.toLowerCase() + '-limit').checked ? 'LIMIT' : 'MARKET')
: (document.getElementById('c-limit').checked ? 'LIMIT' : 'MARKET');
let price = 0;
let amount = 0;
if (tradeType === 'SPOT') {
price = document.getElementById(side.toLowerCase() + '-price').value;
amount = document.getElementById(side.toLowerCase() + '-amount').value;
} else {
price = document.getElementById('c-price').value;
amount = document.getElementById('c-amount').value;
}
try {
const r = await fetch('{% url "core:submit_order" %}', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}' },
body: JSON.stringify({
symbol: symbol, side: side, trade_type: tradeType, order_type: mode,
price: price === '市价价格' ? null : price, amount: amount, leverage: leverage
})
});
const res = await r.json();
if (res.status === 'success') {
alert('下单成功!');
location.reload();
} else {
alert('下单失败: ' + res.message);
}
} catch(e) { alert('提交出错'); }
}
async function closePos(id) {
if (confirm('确定要平仓吗?')) {
const r = await fetch(`/api/close_position/${id}/`, {
method: 'POST',
headers: { 'X-CSRFToken': '{{ csrf_token }}' }
});
const res = await r.json();
if (res.status === 'success') location.reload();
}
}
initTV(); getMarkets(); tick();

View File

@ -1,8 +1,11 @@
from django.urls import path
from . import views
app_name = 'core'
urlpatterns = [
path('', views.index, name='index'),
path('trade/<str:trade_type>/', views.trade, name='trade'),
path('spot/', views.trade, {'trade_type': 'spot'}, name='spot_trade'),
path('contract/', views.trade, {'trade_type': 'contract'}, name='contract_trade'),
path('markets/', views.market_center, name='market_center'),
@ -14,6 +17,10 @@ urlpatterns = [
path('register/', views.register_view, name='register'),
path('api/market_data/', views.market_data, name='market_data'),
path('api/submit_order/', views.submit_order, name='submit_order'),
path('api/close_position/<int:position_id>/', views.close_position, name='close_position'),
# Dynamic Article/Placeholder Route
path('article/<str:title>/', views.placeholder_view, name='placeholder'),
# Footer links
path('help/', views.placeholder_view, {'title': '帮助中心'}, name='help_center'),

View File

@ -1,32 +1,49 @@
from django.shortcuts import render, redirect
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib.auth import login, authenticate
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.http import JsonResponse
from django.contrib.auth.models import User
from django.contrib import messages
from .models import Account, Order, Transaction, Cryptocurrency, SiteSettings
from .models import Account, Order, Transaction, Cryptocurrency, SiteSettings, Asset, Position
import random
import decimal
import json
from django.views.decorators.csrf import csrf_exempt
from django.db import transaction
def index(request):
return render(request, 'core/index.html')
def trade(request, trade_type='spot'):
symbol = request.GET.get('symbol', 'BTCUSDT')
# Fetch all active cryptocurrencies for the sidebar
base_symbol = symbol.replace('USDT', '')
cryptos = Cryptocurrency.objects.filter(is_active=True)
account = None
assets = {}
base_asset_balance = decimal.Decimal('0')
if request.user.is_authenticated:
account, _ = Account.objects.get_or_create(user=request.user)
for asset in account.assets.all():
assets[asset.currency] = asset
if asset.currency == base_symbol:
base_asset_balance = asset.balance
context = {
'symbol': symbol,
'base_symbol': base_symbol,
'trade_type': trade_type.upper(),
'cryptos': cryptos,
'account': account,
'assets': assets,
'base_asset_balance': base_asset_balance,
}
return render(request, 'core/trade.html', context)
def market_center(request):
return render(request, 'core/index.html', {'market_only': True})
cryptos = Cryptocurrency.objects.filter(is_active=True)
return render(request, 'core/market_center.html', {'cryptos': cryptos})
def placeholder_view(request, title):
settings = SiteSettings.objects.first()
@ -61,10 +78,12 @@ def profile(request):
account, created = Account.objects.get_or_create(user=request.user)
recent_transactions = Transaction.objects.filter(account=account).order_by('-timestamp')[:10]
recent_orders = Order.objects.filter(account=account).order_by('-created_at')[:10]
positions = Position.objects.filter(account=account, is_active=True)
context = {
'account': account,
'recent_transactions': recent_transactions,
'recent_orders': recent_orders,
'positions': positions,
}
return render(request, 'core/profile.html', context)
@ -82,7 +101,7 @@ def deposit(request):
status='pending',
tx_hash=tx_id
)
return redirect('profile')
return redirect('core:profile')
return render(request, 'core/deposit.html')
@login_required
@ -103,7 +122,7 @@ def withdraw(request):
status='completed',
tx_hash=f"wd_{random.randint(1000, 9999)}"
)
return redirect('profile')
return redirect('core:profile')
return render(request, 'core/withdraw.html', {'account': account})
@login_required
@ -112,21 +131,9 @@ def verify(request):
if request.method == 'POST':
account.kyc_status = 'pending'
account.save()
return redirect('profile')
return redirect('core:profile')
return render(request, 'core/verify.html', {'account': account})
def generate_captcha():
a = random.randint(1, 10)
b = random.randint(1, 10)
op = random.choice(['+', '-', '*'])
if op == '+':
res = a + b
elif op == '-':
res = a - b
else:
res = a * b
return f"{a} {op} {b} = ?", res
def register_view(request):
if request.method == 'POST':
username = request.POST.get('username')
@ -145,9 +152,8 @@ def register_view(request):
user = User.objects.create_user(username=username, password=password)
Account.objects.get_or_create(user=user)
login(request, user)
return redirect('index')
return redirect('core:index')
# Generate new captcha
captcha_text, captcha_result = generate_captcha()
request.session['captcha_result'] = captcha_result
@ -155,13 +161,22 @@ def register_view(request):
'captcha_text': captcha_text
})
def generate_captcha():
a = random.randint(1, 10)
b = random.randint(1, 10)
op = random.choice(['+', '-', '*'])
if op == '+': res = a + b
elif op == '-': res = a - b
else: res = a * b
return f"{a} {op} {b} = ?", res
def login_view(request):
if request.method == 'POST':
form = AuthenticationForm(data=request.POST)
if form.is_valid():
user = form.get_user()
login(request, user)
return redirect('index')
return redirect('core:index')
else:
form = AuthenticationForm()
return render(request, 'core/login.html', {'form': form})
@ -169,76 +184,181 @@ def login_view(request):
@login_required
@csrf_exempt
def submit_order(request):
if request.method == 'POST':
if request.method != 'POST':
return JsonResponse({'status': 'error', 'message': 'Invalid request'})
try:
data = json.loads(request.body)
symbol = data.get('symbol', 'BTCUSDT')
side = data.get('side')
amount = decimal.Decimal(data.get('amount', 0))
side = data.get('side') # BUY or SELL
trade_type = data.get('trade_type', 'SPOT')
order_type = data.get('order_type', 'MARKET')
price = data.get('price')
price_val = data.get('price')
amount_val = data.get('amount', 0)
leverage = int(data.get('leverage', 20))
account = request.user.account
base_symbol = symbol.replace('USDT', '')
# Fetch current price from DB or simulated
crypto = Cryptocurrency.objects.filter(symbol=symbol.replace('USDT', '')).first()
current_price = crypto.current_price if crypto else decimal.Decimal('48000')
# If market order, use current price. If limit, use provided price.
exec_price = decimal.Decimal(str(price)) if order_type == 'LIMIT' else current_price
# Get current market price for validations and market orders
crypto = Cryptocurrency.objects.filter(symbol=base_symbol).first()
current_price = crypto.current_price if (crypto and crypto.current_price > 0) else decimal.Decimal('48000')
with transaction.atomic():
if trade_type == 'SPOT':
if order_type == 'MARKET':
# Spot Market Order: Execute Immediately
if side == 'BUY':
cost = amount * exec_price
if account.balance >= cost:
account.balance -= cost
account.save()
Order.objects.create(
account=account,
symbol=symbol,
side=side,
amount=amount,
trade_type=trade_type,
order_type=order_type,
status='FILLED',
entry_price=exec_price,
price=exec_price if order_type == 'LIMIT' else None
)
return JsonResponse({'status': 'success'})
else:
# BUY: amount_val is USDT
total_usdt = decimal.Decimal(str(amount_val))
if account.balance < total_usdt:
return JsonResponse({'status': 'error', 'message': '余额不足'})
else:
# SELL
revenue = amount * exec_price
Order.objects.create(
account=account,
symbol=symbol,
side=side,
amount=amount,
trade_type=trade_type,
order_type=order_type,
status='FILLED',
entry_price=exec_price,
price=exec_price if order_type == 'LIMIT' else None
)
account.balance += revenue
exec_amount = total_usdt / current_price
account.balance -= total_usdt
account.save()
asset, _ = Asset.objects.get_or_create(account=account, currency=base_symbol)
asset.balance += exec_amount
asset.save()
Order.objects.create(
account=account, symbol=symbol, side=side, order_type=order_type,
trade_type=trade_type, amount=exec_amount, total_usdt=total_usdt, status='FILLED'
)
else: # SELL
# SELL: amount_val is coin quantity
exec_amount = decimal.Decimal(str(amount_val))
asset, _ = Asset.objects.get_or_create(account=account, currency=base_symbol)
if asset.balance < exec_amount:
return JsonResponse({'status': 'error', 'message': f'{base_symbol} 余额不足'})
total_usdt = exec_amount * current_price
asset.balance -= exec_amount
asset.save()
account.balance += total_usdt
account.save()
Order.objects.create(
account=account, symbol=symbol, side=side, order_type=order_type,
trade_type=trade_type, amount=exec_amount, total_usdt=total_usdt, status='FILLED'
)
else: # LIMIT
# Spot Limit Order: Freeze Assets, Pend
price = decimal.Decimal(str(price_val))
amount = decimal.Decimal(str(amount_val))
if side == 'BUY':
total_usdt = price * amount
if account.balance < total_usdt:
return JsonResponse({'status': 'error', 'message': '余额不足'})
account.balance -= total_usdt
account.frozen_balance += total_usdt
account.save()
Order.objects.create(
account=account, symbol=symbol, side=side, order_type=order_type,
trade_type=trade_type, amount=amount, price=price, total_usdt=total_usdt, status='PENDING'
)
else: # SELL
asset, _ = Asset.objects.get_or_create(account=account, currency=base_symbol)
if asset.balance < amount:
return JsonResponse({'status': 'error', 'message': f'{base_symbol} 余额不足'})
asset.balance -= amount
asset.frozen += amount
asset.save()
Order.objects.create(
account=account, symbol=symbol, side=side, order_type=order_type,
trade_type=trade_type, amount=amount, price=price, status='PENDING'
)
else: # CONTRACT
# Contract: Initial Margin = face_value(100) * lots / leverage
lots = decimal.Decimal(str(amount_val))
face_value = decimal.Decimal('100')
margin_required = (face_value * lots) / decimal.Decimal(str(leverage))
if account.balance < margin_required:
return JsonResponse({'status': 'error', 'message': '保证金不足'})
if order_type == 'MARKET':
# Contract Market Order: Immediate Open Position
account.balance -= margin_required
account.save()
Position.objects.create(
account=account, symbol=symbol, side='LONG' if side == 'BUY' else 'SHORT',
leverage=leverage, entry_price=current_price, lots=lots, margin=margin_required
)
Order.objects.create(
account=account, symbol=symbol, side=side, order_type=order_type,
trade_type=trade_type, amount=lots, leverage=leverage, status='FILLED'
)
else: # LIMIT
# Contract Limit Order: Freeze Margin, Pend
price = decimal.Decimal(str(price_val))
account.balance -= margin_required
account.frozen_balance += margin_required
account.save()
Order.objects.create(
account=account, symbol=symbol, side=side, order_type=order_type,
trade_type=trade_type, amount=lots, price=price, leverage=leverage, status='PENDING'
)
return JsonResponse({'status': 'success'})
except Exception as e:
return JsonResponse({'status': 'error', 'message': str(e)})
@login_required
@csrf_exempt
def close_position(request, position_id):
if request.method != 'POST':
return JsonResponse({'status': 'error', 'message': 'Invalid request'})
position = get_object_or_404(Position, id=position_id, account=request.user.account, is_active=True)
base_symbol = position.symbol.replace('USDT', '')
crypto = Cryptocurrency.objects.filter(symbol=base_symbol).first()
current_price = crypto.current_price if (crypto and crypto.current_price > 0) else decimal.Decimal('48000')
face_value = decimal.Decimal('100')
# Calculate Unrealized P&L
if position.side == 'LONG':
upl = (current_price - position.entry_price) / position.entry_price * (position.lots * face_value)
else:
upl = (position.entry_price - current_price) / position.entry_price * (position.lots * face_value)
with transaction.atomic():
account = request.user.account
# Settlement: Margin + P&L - Fee (0.05%)
fee = (position.lots * face_value) * decimal.Decimal('0.0005')
account.balance += (position.margin + upl - fee)
account.save()
position.is_active = False
position.save()
# Log closing order
Order.objects.create(
account=account, symbol=position.symbol, side='SELL' if position.side == 'LONG' else 'BUY',
order_type='MARKET', trade_type='CONTRACT', amount=position.lots, status='FILLED'
)
return JsonResponse({'status': 'success'})
def market_data(request):
# This might be used by some local scripts, though index/trade use Binance API directly
symbols = ['BTC', 'ETH', 'BNB', 'SOL', 'ADA', 'XRP', 'DOT', 'DOGE']
cryptos = Cryptocurrency.objects.filter(is_active=True)
data = []
for s in symbols:
price = random.uniform(10, 60000)
change = random.uniform(-5, 5)
for c in cryptos:
data.append({
'symbol': s,
'price': round(price, 4),
'change': round(change, 2)
'symbol': c.symbol,
'price': float(c.current_price),
'change': float(c.change_24h)
})
return JsonResponse(data, safe=False)