diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc index 000b231..bd209b1 100644 Binary files a/core/__pycache__/context_processors.cpython-311.pyc and b/core/__pycache__/context_processors.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index d3e41d7..782412c 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index af3b707..fa55f1d 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 84d71d7..7b6c4cf 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/context_processors.py b/core/context_processors.py index c0cf4a5..b76295e 100644 --- a/core/context_processors.py +++ b/core/context_processors.py @@ -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 \ No newline at end of file diff --git a/core/migrations/0006_remove_order_close_price_remove_order_entry_price_and_more.py b/core/migrations/0006_remove_order_close_price_remove_order_entry_price_and_more.py new file mode 100644 index 0000000..e32ffa9 --- /dev/null +++ b/core/migrations/0006_remove_order_close_price_remove_order_entry_price_and_more.py @@ -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': '持仓管理', + }, + ), + ] diff --git a/core/migrations/__pycache__/0006_remove_order_close_price_remove_order_entry_price_and_more.cpython-311.pyc b/core/migrations/__pycache__/0006_remove_order_close_price_remove_order_entry_price_and_more.cpython-311.pyc new file mode 100644 index 0000000..b9f16c6 Binary files /dev/null and b/core/migrations/__pycache__/0006_remove_order_close_price_remove_order_entry_price_and_more.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index a9c352c..759b4b8 100644 --- a/core/models.py +++ b/core/models.py @@ -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', _('失败'))) @@ -141,4 +157,4 @@ class Transaction(models.Model): class Meta: verbose_name = _("充提记录") - verbose_name_plural = _("充提管理") \ No newline at end of file + verbose_name_plural = _("充提管理") diff --git a/core/templates/base.html b/core/templates/base.html index 43afb9b..b55a06f 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -100,10 +100,10 @@ @@ -149,32 +149,32 @@
产品服务
法律法规
客户支持
关于我们
diff --git a/core/templates/core/index.html b/core/templates/core/index.html index 3cb6fda..26b3984 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -18,8 +18,8 @@

开启您的
加密货币之旅

在全球最受信任的交易平台买卖和存储加密货币。BitCrypto 为您提供安全、稳定、高效的服务。

- 立即注册 - 开始交易 + 立即注册 + 开始交易
@@ -32,7 +32,7 @@

领先的衍生品交易平台

最高200倍杠杆,支持多种永续合约,毫秒级撮合引擎。多空双向交易,灵活捕捉市场机会。

- 进入合约交易 + 进入合约交易
@@ -98,7 +98,7 @@

热门行情

实时获取全球顶级加密货币价格走势

- 查看更多市场 + 查看更多市场
diff --git a/core/templates/core/login.html b/core/templates/core/login.html index 8d6ce2c..7798100 100644 --- a/core/templates/core/login.html +++ b/core/templates/core/login.html @@ -41,7 +41,7 @@
-

还没有账户? 立即注册

+

还没有账户? 立即注册

diff --git a/core/templates/core/market_center.html b/core/templates/core/market_center.html new file mode 100644 index 0000000..d41f222 --- /dev/null +++ b/core/templates/core/market_center.html @@ -0,0 +1,107 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+

行情中心

+
+ + +
+
+ +
+
+ + + + + + + + + + + + + + {% for crypto in cryptos %} + + + + + + + + + + {% endfor %} + +
币种最新价24h 涨跌24h 最高24h 最低24h 成交额操作
+
+ +
+
{{ crypto.symbol }}
+
{{ crypto.name }}
+
+
+
---------- + 现货 + 合约 +
+
+
+
+ + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/core/templates/core/profile.html b/core/templates/core/profile.html index 437e3ee..f472f2a 100644 --- a/core/templates/core/profile.html +++ b/core/templates/core/profile.html @@ -23,8 +23,8 @@
@@ -122,7 +122,7 @@
身份认证 (KYC)

提升提现额度至 100 BTC

- 立即认证 + 立即认证 diff --git a/core/templates/core/register.html b/core/templates/core/register.html index 2aaa8be..6c189de 100644 --- a/core/templates/core/register.html +++ b/core/templates/core/register.html @@ -66,7 +66,7 @@
@@ -75,7 +75,7 @@
-

已有账户? 立即登录

+

已有账户? 立即登录

diff --git a/core/templates/core/trade.html b/core/templates/core/trade.html index 912103a..390bc34 100644 --- a/core/templates/core/trade.html +++ b/core/templates/core/trade.html @@ -57,8 +57,8 @@
- 现货 - 合约 + 现货 + 合约
@@ -68,7 +68,7 @@ -
+
{% if trade_type == 'SPOT' %}
@@ -87,9 +87,9 @@
- 成交额 - - USDT + 数量 + + {{ base_symbol }}
@@ -103,7 +103,7 @@ 可用余额 {{ account.balance|default:"0.00" }} USDT
- +
@@ -124,7 +124,7 @@
数量 - {{ symbol|slice:":-4" }} + {{ base_symbol }}
@@ -135,10 +135,10 @@
- 可用 {{ symbol|slice:":-4" }} - 0.00 + 可用 {{ base_symbol }} + {{ base_asset_balance|default:"0.00" }} {{ base_symbol }}
- +
{% else %} @@ -147,9 +147,9 @@
- + - +
手数 - +
-
+
+
+ 0%25%50%75%100% +
- 保证金: 0.00 USDT + 所需保证金: 0.00 USDT 可用: {{ account.balance|default:"0.00" }} USDT
-
-
+
+
-
合约面值100 USDT
+
合约面值100 USDT / 张
当前杠杆20x
预计手续费0.05%
-
维持保证金0.4%
+
维持保证金率0.4%
{% endif %} + + +
+ +
+
+
+ + + + + + + + + + + + + + + {% if trade_type == 'CONTRACT' %} + {% for p in account.positions.all %} + {% if p.is_active %} + + + + + + + + + + + {% endif %} + {% endfor %} + {% endif %} + {% for o in account.orders.all %} + {% if o.status == 'PENDING' %} + + + + + + + + + + + {% endif %} + {% endfor %} + +
合约/现货方向数量/手数开仓/委托价当前价保证金盈亏操作
{{ p.symbol }} {{ p.leverage }}x{{ p.get_side_display }}{{ p.lots }}{{ p.entry_price }}--{{ p.margin|floatformat:2 }}--
{{ o.symbol }} ({{ o.get_trade_type_display }}){{ o.get_side_display }}{{ o.amount }}{{ o.price|default:"市价" }}----等待成交
+
+
+
+
@@ -202,7 +266,7 @@
价格(USDT) - 数量({{ symbol|slice:":-4" }}) + 数量({{ base_symbol }})
@@ -216,7 +280,7 @@ {% 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 = ` - ${base} - ${parseFloat(c.lastPrice).toFixed(2)} + ${base} + ${parseFloat(c.lastPrice).toLocaleString()} ${chg>=0?'+':''}${chg.toFixed(2)}% `; list.innerHTML += row; - mobileList.innerHTML += `
  • ${base}${parseFloat(c.lastPrice).toFixed(2)}
  • `; + mobileList.innerHTML += `
  • ${base}${parseFloat(c.lastPrice).toLocaleString()}
  • `; }); } @@ -280,32 +349,90 @@ } function toggleMode(side, mode) { - const pInput = document.getElementById(side === 'contract' ? 'c-price' : side + '-price'); - if (mode === 'market') { - pInput.value = '市价价格'; - pInput.disabled = true; - pInput.style.color = '#f0b90b'; - } else { - pInput.value = currentPrice || ''; - pInput.disabled = false; - pInput.style.color = 'white'; + 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; + pInput.style.color = '#f0b90b'; + } else { + pInput.value = currentPrice || ''; + pInput.disabled = false; + 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,21 +462,75 @@ bidD.innerHTML += `
    ${(currentPrice-(i+1)*0.1).toFixed(2)}${(Math.random()*1.2).toFixed(4)}
    `; } - // 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(); setInterval(tick, 2000); -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 3c7eeea..36bca48 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,8 +1,11 @@ from django.urls import path from . import views +app_name = 'core' + urlpatterns = [ path('', views.index, name='index'), + path('trade//', 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//', views.close_position, name='close_position'), + + # Dynamic Article/Placeholder Route + path('article//', views.placeholder_view, name='placeholder'), # Footer links path('help/', views.placeholder_view, {'title': '帮助中心'}, name='help_center'), diff --git a/core/views.py b/core/views.py index 270d68f..bb6bb3d 100644 --- a/core/views.py +++ b/core/views.py @@ -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': - try: - data = json.loads(request.body) - symbol = data.get('symbol', 'BTCUSDT') - side = data.get('side') - amount = decimal.Decimal(data.get('amount', 0)) - trade_type = data.get('trade_type', 'SPOT') - order_type = data.get('order_type', 'MARKET') - price = data.get('price') + 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') # BUY or SELL + trade_type = data.get('trade_type', 'SPOT') + order_type = data.get('order_type', 'MARKET') + 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', '') + + # 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': + # BUY: amount_val is USDT + total_usdt = decimal.Decimal(str(amount_val)) + if account.balance < total_usdt: + return JsonResponse({'status': 'error', 'message': '余额不足'}) + + 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' + ) - account = request.user.account - - # 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 - - 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: - 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 - account.save() - return JsonResponse({'status': 'success'}) + 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)) - except Exception as e: - return JsonResponse({'status': 'error', 'message': str(e)}) - return JsonResponse({'status': 'error', 'message': 'Invalid request'}) + 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) \ No newline at end of file