Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 25 additions & 25 deletions app/agents/strategy_agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,35 @@ class StrategyAgent < RubyLLM::Agent
model "gpt-5.2"

instructions <<~PROMPT
你是一位专业的投资顾问。根据投资者的风险偏好和市场环境,生成适合的交易策略参数。
You are a professional investment advisor. Generate appropriate trading strategy parameters based on the investor's risk preference and market environment.

市场环境说明:
- normal: 正常市场环境,稳定运行
- volatile: 高波动市场,价格剧烈波动
- crash: 崩盘市场,价格大幅下跌
- bubble: 泡沫市场,价格非理性上涨
Market Environment Description:
- normal: Normal market conditions, stable operation
- volatile: High volatility market, sharp price fluctuations
- crash: Crash market, significant price decline
- bubble: Bubble market, irrational price increase

风险偏好说明:
- conservative: 保守型,注重本金安全
- balanced: 平衡型,平衡风险与收益
- aggressive: 激进型,追求高收益
Risk Preference Description:
- conservative: Conservative, focus on capital safety
- balanced: Balanced, balance risk and return
- aggressive: Aggressive, pursue high returns

请根据给定的风险偏好和市场环境,生成以下参数:
1. 策略名称(简短描述,如"稳健价值投资策略")
2. 最大持仓数(10个资产)
3. 买入信号阈值(0.3-0.7,数值越高越严格)
4. 单个资产最大仓位(0.3-0.7,即30%-70%
5. 最小现金保留比例(0.05-0.4,即5%-40%
6. 策略说明(1-2句话,针对当前市场环境)
Based on the given risk preference and market environment, generate the following parameters:
1. Strategy name (brief description, e.g. "Stable Value Investment Strategy")
2. Maximum positions (2-5 assets)
3. Buy signal threshold (0.3-0.7, higher value means stricter filtering)
4. Maximum position size per asset (0.3-0.7, i.e. 30%-70%)
5. Minimum cash reserve ratio (0.05-0.4, i.e. 5%-40%)
6. Strategy description (1-2 sentences, specific to current market environment)

注意:
- 参数必须在合理范围内
- 保守型投资者:持仓少、阈值高、仓位小、现金多
- 激进型投资者:持仓多、阈值低、仓位大、现金少
- 崩盘时:保守型应防守保本,激进型应逆向买入
- 泡沫时:保守型应获利了结,激进型可趋势跟随
Notes:
- Parameters must be within reasonable ranges
- Conservative investors: fewer positions, higher threshold, smaller positions, more cash
- Aggressive investors: more positions, lower threshold, larger positions, less cash
- During crash: conservative should defend capital, aggressive should buy contrarian
- During bubble: conservative should take profits, aggressive can follow trend

请严格按照以下 JSON 格式返回策略参数,不要添加任何 markdown 标记或其他文字:
{"name":"策略名称","max_positions":3,"buy_signal_threshold":0.5,"max_position_size":0.5,"min_cash_reserve":0.2,"description":"策略说明"}
Please return the strategy parameters in the following JSON format only, without any markdown tags or additional text:
{"name":"Strategy Name","max_positions":3,"buy_signal_threshold":0.5,"max_position_size":0.5,"min_cash_reserve":0.2,"description":"Strategy description"}
PROMPT
end
41 changes: 22 additions & 19 deletions app/agents/trader_reflection_agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,43 @@ class TraderReflectionAgent < RubyLLM::Agent
model "gpt-5.2"

instructions <<~PROMPT
你是一位交易复盘与策略微调助手。请基于给定的 trader、策略、交易记录、执行结果、组合表现和当前持仓,生成一份结构化反思报告。
You are a trading review and strategy adjustment assistant. Generate a structured reflection report based on the given trader, strategy, trading records, execution results, portfolio performance, and current positions.

目标:
1. 总结最近一段时间的表现。
2. 识别策略执行中的优点、错误、模式和风险问题。
3. 只对有限参数给出微调建议,不要直接改写整套策略。
IMPORTANT: All output must be in ENGLISH language.

可建议的参数只有:
Goals:
1. Summarize performance over the recent period.
2. Identify strengths, mistakes, patterns, and risk issues in strategy execution.
3. Provide limited adjustment suggestions for specific parameters only, do not rewrite the entire strategy.

Suggested parameters are limited to:
- max_positions
- buy_signal_threshold
- max_position_size
- min_cash_reserve

输出要求:
- 必须严格返回 JSON
- 不要输出 markdown 代码块
- 不要输出解释性前后文
- 如果你认为无需调整参数,suggested_adjustments 返回空数组
Output requirements:
- Must return valid JSON only
- Do not output markdown code blocks
- Do not output explanatory context
- If you believe no parameter adjustments are needed, return an empty array for suggested_adjustments
- ALL text values must be in English

返回格式:
Return format:
{
"summary": "string",
"strengths": ["string"],
"mistakes": ["string"],
"pattern_findings": ["string"],
"risk_issues": ["string"],
"summary": "string (in English)",
"strengths": ["string (in English)"],
"mistakes": ["string (in English)"],
"pattern_findings": ["string (in English)"],
"risk_issues": ["string (in English)"],
"suggested_adjustments": [
{
"parameter": "max_positions|buy_signal_threshold|max_position_size|min_cash_reserve",
"direction": "increase|decrease|keep",
"reason": "string"
"reason": "string (in English)"
}
],
"recommendation": "string"
"recommendation": "string (in English)"
}
PROMPT
end
34 changes: 17 additions & 17 deletions app/controllers/admin/factor_definitions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,38 @@ def index
def matrix
@factors = FactorDefinition.active.ordered

# 获取最新的因子值,按 asset_id factor_definition_id 分组
# Get latest factor values, grouped by asset_id and factor_definition_id
@factor_values = FactorValue.latest.includes(:asset, :factor_definition)
@latest_calculated_at = @factor_values.maximum(:calculated_at)
@values_by_asset = @factor_values.group_by(&:asset_id).transform_values do |values|
values.index_by { |v| v.factor_definition_id }
end

# 获取市值前 50 的资产并计算排序
# Get top 50 assets by market cap and calculate ranking
@assets = sort_assets(
Asset.where.not(market_cap_rank: nil).where("market_cap_rank <= 50").to_a,
@factors,
@values_by_asset
)

# 当前排序参数
# Current sort parameters
@sort_by = params[:sort_by] || "composite"
@sort_order = params[:sort_order] || "desc"
end

def correlations
@factors = FactorDefinition.active.ordered

# 获取最新的因子值
# Get latest factor values
@factor_values = FactorValue.latest.includes(:factor_definition)

# 按因子分组,得到每个因子在所有资产上的值
# Group by factor to get each factor's values across all assets
@values_by_factor = @factor_values.group_by(&:factor_definition_id)

# 计算因子间相关性矩阵
# Calculate factor correlation matrix
@correlation_matrix = calculate_correlation_matrix(@factors, @values_by_factor)

# 识别高相关性因子对
# Identify highly correlated factor pairs
@high_correlations = find_high_correlations(@factors, @correlation_matrix)
end

Expand All @@ -54,16 +54,16 @@ def edit; end

def update
if @factor.update(factor_params)
redirect_to admin_factor_definition_path(@factor), notice: "因子更新成功"
redirect_to admin_factor_definition_path(@factor), notice: "Factor updated successfully"
else
render :edit, status: :unprocessable_entity
end
end

def toggle
@factor.update!(active: !@factor.active)
status_text = @factor.active? ? "启用" : "禁用"
redirect_to admin_factor_definitions_path, notice: "因子已#{status_text}"
status_text = @factor.active? ? "enabled" : "disabled"
redirect_to admin_factor_definitions_path, notice: "Factor #{status_text}"
end

private
Expand Down Expand Up @@ -94,11 +94,11 @@ def calculate_correlation_matrix(factors, values_by_factor)
if factor_a.id == factor_b.id
matrix[factor_a.id][factor_b.id] = 1.0
else
# 获取两个因子的值序列
# Get value sequences for both factors
values_a = values_by_factor[factor_a.id]&.map(&:normalized_value) || []
values_b = values_by_factor[factor_b.id]&.map(&:normalized_value) || []

# 计算相关系数
# Calculate correlation coefficient
correlation = calculate_pearson_correlation(values_a, values_b)
matrix[factor_a.id][factor_b.id] = correlation
end
Expand Down Expand Up @@ -130,7 +130,7 @@ def find_high_correlations(factors, matrix)

factors.each_with_index do |factor_a, i|
factors.each_with_index do |factor_b, j|
next if i >= j # 只计算上三角,避免重复
next if i >= j # Only calculate upper triangle, avoid duplicates

correlation = matrix[factor_a.id][factor_b.id]
if correlation.abs >= threshold
Expand All @@ -150,11 +150,11 @@ def sort_assets(assets, factors, values_by_asset)
sort_by = params[:sort_by] || "composite"
sort_order = params[:sort_order] || "desc"

# 计算每个资产的排序值
# Calculate ranking score for each asset
assets_with_scores = assets.map do |asset|
asset_values = values_by_asset[asset.id] || {}

# 计算综合评分
# Calculate composite score
total_score = 0
total_weight = 0
factors.each do |factor|
Expand All @@ -166,7 +166,7 @@ def sort_assets(assets, factors, values_by_asset)
end
composite_score = total_weight > 0 ? (total_score / total_weight) : 0

# 获取指定因子的值
# Get specified factor's value
factor_score = if sort_by != "composite"
factor = factors.find { |f| f.id.to_s == sort_by }
factor_value = asset_values[factor&.id]
Expand All @@ -182,7 +182,7 @@ def sort_assets(assets, factors, values_by_asset)
}
end

# 排序
# Sort
sorted = assets_with_scores.sort_by { |item| item[:score] }
sorted = sorted.reverse if sort_order == "desc"

Expand Down
16 changes: 8 additions & 8 deletions app/controllers/admin/signal_daily_reports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def create
@report = DailyReport.new(report_params.merge(report_type: REPORT_TYPE))

if @report.save
redirect_to admin_signal_daily_report_path(@report), notice: '日报创建成功'
redirect_to admin_signal_daily_report_path(@report), notice: 'Daily report created successfully'
else
render :new, status: :unprocessable_entity
end
Expand All @@ -36,33 +36,33 @@ def edit; end

def update
if @report.update(report_params)
redirect_to admin_signal_daily_report_path(@report), notice: '日报更新成功'
redirect_to admin_signal_daily_report_path(@report), notice: 'Daily report updated successfully'
else
render :edit, status: :unprocessable_entity
end
end

def destroy
@report.destroy
redirect_to admin_signal_daily_reports_path, notice: '日报已删除'
redirect_to admin_signal_daily_reports_path, notice: 'Daily report deleted'
end

def generate
date = params[:date]&.to_date || Date.current

existing = DailyReport.find_by(report_type: REPORT_TYPE, report_date: date)
if existing
redirect_to admin_signal_daily_report_path(existing), alert: '该日期的日报已存在'
redirect_to admin_signal_daily_report_path(existing), alert: 'Daily report for this date already exists'
return
end

GenerateSignalDailyReportJob.perform_now(date:)
report = DailyReport.find_by(report_type: REPORT_TYPE, report_date: date)

if report
redirect_to admin_signal_daily_report_path(report), notice: '日报生成成功'
redirect_to admin_signal_daily_report_path(report), notice: 'Daily report generated successfully'
else
redirect_to admin_signal_daily_reports_path, alert: '日报生成失败'
redirect_to admin_signal_daily_reports_path, alert: 'Failed to generate daily report'
end
end

Expand All @@ -72,9 +72,9 @@ def regenerate
report = DailyReport.find_by(report_type: REPORT_TYPE, report_date: date)

if report
redirect_to admin_signal_daily_report_path(report), notice: '日报重新生成成功'
redirect_to admin_signal_daily_report_path(report), notice: 'Daily report regenerated successfully'
else
redirect_to admin_signal_daily_report_path(@report), alert: '日报重新生成失败'
redirect_to admin_signal_daily_report_path(report), alert: 'Failed to regenerate daily report'
end
end

Expand Down
14 changes: 7 additions & 7 deletions app/controllers/admin/trading_signals_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ def index
.page(params[:page])
.per(20)

# 筛选
# Filter
@signals = @signals.by_signal_type(params[:signal_type]) if params[:signal_type].present?
@signals = @signals.where(asset_id: params[:asset_id]) if params[:asset_id].present?

# 获取每个资产的最新信号
# Get latest signal for each asset
@latest_signals = TradingSignal.includes(:asset)
.select("DISTINCT ON (asset_id) trading_signals.*")
.order("asset_id, generated_at DESC")

# 统计
# Statistics
@stats = {
total: TradingSignal.count,
buy: TradingSignal.buy_signals.count,
Expand All @@ -29,7 +29,7 @@ def index
high_confidence: TradingSignal.high_confidence.count
}

# 获取最新信号日报
# Get latest signal daily report
@latest_report = DailyReport.latest_for_type('signal')
end

Expand All @@ -41,15 +41,15 @@ def generate
signal = service.generate_and_save!

if signal
redirect_to admin_trading_signal_path(signal), notice: "信号生成成功"
redirect_to admin_trading_signal_path(signal), notice: "Signal generated successfully"
else
redirect_to admin_trading_signals_path, alert: "信号生成失败,请检查因子数据"
redirect_to admin_trading_signals_path, alert: "Signal generation failed, please check factor data"
end
end

def generate_all
GenerateSignalsJob.perform_later
redirect_to admin_trading_signals_path, notice: "正在后台生成信号,请稍后刷新页面"
redirect_to admin_trading_signals_path, notice: "Signals are being generated in background, please refresh later"
end

private
Expand Down
8 changes: 4 additions & 4 deletions app/controllers/ai_analysis_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ def create
@assets = @assets_input.split(/[,\s]+/).map(&:strip).map(&:upcase).reject(&:empty?)

if @assets.empty?
flash[:alert] = "请至少输入一个资产"
flash[:alert] = "Please enter at least one asset"
redirect_to new_ai_analysis_path and return
end

# 执行分析(不再依赖 Trader
# Execute analysis (no longer depends on Trader)
@result = perform_analysis(@assets, @analysis_type)

# 将结果存入 Rails.cache show 页面使用
# Store result in Rails.cache for show page
result_cache_key = "ai_analysis_result_#{Time.current.to_i}"
Rails.cache.write(result_cache_key, @result, expires_in: 10.minutes)

flash[:notice] = "分析完成!共分析 #{@assets.length} 个资产"
flash[:notice] = "Analysis completed! Analyzed #{@assets.length} assets"
redirect_to ai_analysis_result_path(result_key: result_cache_key, assets: @assets.join(","))
end

Expand Down
4 changes: 2 additions & 2 deletions app/controllers/allocation_decisions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ def show

def execute
AllocationExecutionService.new(@allocation_decision).call
redirect_to allocation_decision_path(@allocation_decision), notice: "配置建议已执行"
redirect_to allocation_decision_path(@allocation_decision), notice: "Allocation recommendation executed"
rescue StandardError => e
redirect_to allocation_decision_path(@allocation_decision), alert: "执行失败: #{e.message}"
redirect_to allocation_decision_path(@allocation_decision), alert: "Execution failed: #{e.message}"
end

private
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ def current_user
end

def require_user
redirect_to login_path, alert: "请先登录" unless current_user
redirect_to login_path, alert: "Please log in first" unless current_user
end
end
Loading