1.开发背景
在东方财富网的数据交易中,针对自己关注的股票能够看到实际的交易成交量。但是交易量无法直观的看出来,而且当交易出现大批量的变动。例如出现逼空、大单买入及主力拉升等行情时无法及时通知投资者。本篇文章介绍的软件中投资者能够自定义股票预警行情,当出现较大的行情波动时能够通过弹窗和表格的两种形式提示投资者。
2.使用技术
在该软件中使用C#调用Selenium模拟打开网页,在打开网页之后通过解析网络请求获取到当日东方财富网的数据URL(因为东方财富网每日的URL是动态变化的)。在解析对应的URL后,软件通过定时任务自动请求当日、3日、5日以及10交易数据。
软件将获取到的数据按照原始数据格式保存至本地SQLLite数据库中,数据库采用按日进行分库的方式。每个数据库中仅保存当日数据,避免由于数据过大导致数据库损坏后无法打开的问题。
投资者在软件可设置同一时间的预警条件。能够实现3日主力与3日小单等条件的设置,也可设置同意线性的变化,例如当前3日主力与上一次3日主力交易变化。当达到预警条件后,系统在基础信息表中根据预警类别显示为不同的颜色,以达到通过颜色就能够区分出来时间紧急度的目的。
3.实现技术
3.1 数据获取
通过以下代码获取东方财富网交易URL
private void InitUrls()
{
EdgeDriver driver = null;
try
{
var service = EdgeDriverService.CreateDefaultService(@".", "msedgedriver.exe");
EdgeOptions options = new();
options.AddArguments("--test-type", "--ignore-certificate-errors");
options.SetLoggingPreference("performance", OpenQA.Selenium.LogLevel.Info); //启用performance日志,等级为Info即可
options.PerformanceLoggingPreferences = new OpenQA.Selenium.Chromium.ChromiumPerformanceLoggingPreferences
{
IsCollectingNetworkEvents = true
};
driver = new EdgeDriver(service, options, TimeSpan.FromSeconds(120));
//默认是深圳股票
string urlMain = "https://data.eastmoney.com/zjlx/detail.html";
driver.Navigate().GoToUrl(urlMain);
var source = driver.PageSource;
var current = driver.FindElement(By.XPath("//*[@id=\"filter_stat\"]"));
var childrens = current.FindElements(By.XPath("./*"));// 找到所有子元素
foreach (var item in childrens)
{
if (item.Text.Contains("今日排行") ||
item.Text.Contains("3日排行") ||
item.Text.Contains("5日排行") ||
item.Text.Contains("10日排行"))
{
string text = item.Text;
item.Click();
Thread.Sleep(1000);
Dictionary<string, string>? contents = NetworkLoggingHelper.GetNetworkApiDatas(driver, Filter1, "", "排行");
current = driver.FindElement(By.XPath("//*[@class=\"pagerbox\"]"));
var achildrens = current.FindElements(By.XPath("./*"));// 找到所有子元素
var maxPage = 0;
foreach (var achild in achildrens)
{
if (int.TryParse(achild.Text, out int value))
{
if (value > maxPage)
{
maxPage = value;
}
}
}
if (contents.Count != 0)
{
foreach (var itemDic in contents)
{
var model = new SourcesData
{
Data = itemDic.Value,
Url = itemDic.Key,
Text = item.Text,
MaxPage = maxPage
};
// 分割URL以获取查询字符串部分
string query = new Uri(itemDic.Key).Query;
// 移除查询字符串前的问号
query = query.TrimStart('?');
// 使用NameValueCollection来解析查询字符串
var parameters = HttpUtility.ParseQueryString(query);
string preRepleace = "";
// 遍历并打印参数名和值
foreach (string key in parameters.AllKeys)
{
if (key == "cb")
{
preRepleace = parameters[key];
break;
}
}
int pz = 50;
foreach (string key in parameters.AllKeys)
{
if (key == "pz")
{
pz = Convert.ToInt32(parameters[key]);
break;
}
}
model.Data = model.Data.Replace(preRepleace + "(", "").Replace(");", "");
StockRankInfo<StockRankingBase>? obj = JsonConvert.DeserializeObject<StockRankInfo<StockRankingBase>>(model.Data);
maxPage = obj.data.total / 500;
if (obj.data.total % 500 != 0)
{
maxPage += 1;
}
if (urlContents.ContainsKey(item.Text))
{
urlContents[item.Text] = model;
}
else
{
urlContents.Add(item.Text, model);
}
break;
}
}
}
}
}
catch (Exception ex)
{
Logger.Error("初始化数据连接出现异常", ex);
}
finally
{
if (driver != null)
{
driver.Quit();
}
}
}
3.2 数据自动保存
自动将各个数据保存至数据库表中,通过不同的数据类型区分
string timeDb = DateTime.Now.ToString("yyyyMMdd");
TransDataDB dbClient = new TransDataDB("transData", timeDb);
DateTime saveTime = DateTime.Now;
string timeTmp = saveTime.ToString("yyyy-MM-dd HH:mm:ss");
var result = GetRankingInfo<StockRankingBase>(item.Value.Url, 1, "", item.Value.MaxPage);
var sql = "INSERT INTO StockRankings(rank_type,create_date , f1, f2, f3, f12, f13, f14, f62, f66, f69, f72, f75, f78, f81, f84, f87, f124, f184)VALUES";
var deleteSql = "delete from StockRankings where rank_type='today' and create_date='" + timeTmp + "'";
dbClient.SaveDataToSQLliteDB(deleteSql);
foreach (var days in result.data.diff)
{
sql += string.Format("('today','{0}',{1}, {2}, {3}, '{4}', {5}, '{6}', {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, {15},{16}, {17})",
timeTmp, days.f1,
string.IsNullOrWhiteSpace(days.f2) || !decimal.TryParse(days.f2, out decimal f2) ? 0 : f2,
string.IsNullOrWhiteSpace(days.f3) || !decimal.TryParse(days.f2, out decimal f3) ? 0 : days.f3,
string.IsNullOrWhiteSpace(days.f12) ? 0 : days.f12,
string.IsNullOrWhiteSpace(days.f13) || !decimal.TryParse(days.f2, out decimal f13) ? 0 : days.f13,
string.IsNullOrWhiteSpace(days.f14) ? 0 : days.f14,
string.IsNullOrWhiteSpace(days.f62) || !decimal.TryParse(days.f62, out decimal f62) ? 0 : days.f62,
string.IsNullOrWhiteSpace(days.f66) || !decimal.TryParse(days.f66, out decimal f66) ? 0 : days.f66,
string.IsNullOrWhiteSpace(days.f69) || !decimal.TryParse(days.f69, out decimal f69) ? 0 : days.f69,
string.IsNullOrWhiteSpace(days.f72) || !decimal.TryParse(days.f72, out decimal f72) ? 0 : days.f72,
string.IsNullOrWhiteSpace(days.f75) || !decimal.TryParse(days.f75, out decimal f75) ? 0 : days.f75,
string.IsNullOrWhiteSpace(days.f78) || !decimal.TryParse(days.f78, out decimal f78) ? 0 : f78,
string.IsNullOrWhiteSpace(days.f81) || !decimal.TryParse(days.f81, out decimal f81) ? 0 : days.f81,
string.IsNullOrWhiteSpace(days.f84) || !decimal.TryParse(days.f84, out decimal f84) ? 0 : days.f84,
string.IsNullOrWhiteSpace(days.f87) || !decimal.TryParse(days.f87, out decimal f87) ? 0 : days.f87,
string.IsNullOrWhiteSpace(days.f124) || !decimal.TryParse(days.f124, out decimal f124) ? 0 : days.f124,
string.IsNullOrWhiteSpace(days.f184) || !decimal.TryParse(days.f184, out decimal f184) ? 0 : days.f184
) + ",";
}
sql = sql.TrimEnd(',');
if (dbClient.SaveDataToSQLliteDB(sql))
{
ShowLod("保存今日排行数据成功");
}
else
{
ShowLod("保存今日排行数失败");
}
3.3 自动预警
该软件通过读取软件中保存的预警条件,定时查询数据。当达到预警条件后执行弹窗预警
if (BaseControl.ConditionList == null || BaseControl.ConditionList.Count == 0)
{
return;
}
var intersectionList = new Dictionary<string, List<ConditionInfo>>();
if (BaseControl.ConditionList.Count > 0)
{
var groupedEmployees = from e in BaseControl.ConditionList
group e by e.GroupName into g
select new
{
GroupName = g.Key,
ConditionList = g.ToList()
};
foreach (var item in groupedEmployees)
{
var infos = new List<ConditionInfo>();
foreach (var condition in BaseControl.ConditionVerticalList)
{
if (item.GroupName == condition.GroupName)
{
condition.GroupType = "纵向预警";
infos.Add(condition);
}
}
if (infos.Count > 0)// 表示有交集,需要更复杂的查询语句
{
if (intersectionList.ContainsKey(item.GroupName))
{
infos.AddRange(item.ConditionList);
intersectionList[item.GroupName] = infos;
}
else
{
infos.AddRange(item.ConditionList);
intersectionList.Add(item.GroupName, infos);
}
}
}
}
if (BaseControl.ConditionVerticalList.Count > 0)
{
var groups = from e in BaseControl.ConditionVerticalList
group e by e.GroupName into g
select new
{
GroupName = g.Key,
ConditionList = g.ToList()
};
foreach (var group in groups)
{
if (!intersectionList.ContainsKey(group.GroupName))// 如果没有这个分组,需要单独进行多方位预警
{
var infos = new List<ConditionInfo>();
foreach (var item in group.ConditionList)
{
item.GroupType = "纵向预警";
infos.Add(item);
}
intersectionList.Add(group.GroupName, infos);
}
}
}
var loopItems = new Dictionary<string, List<ConditionInfo>>();
{
var groupedEmployees = from e in BaseControl.ConditionList
group e by e.GroupName into g
select new
{
GroupName = g.Key,
ConditionList = g.ToList()
};
foreach (var item in groupedEmployees)
{
loopItems.Add(item.GroupName, item.ConditionList);
}
}
StringBuilder sb = new StringBuilder();
sb.Append("SELECT ");
if (BaseControl.ConditionList.Count > 0)
{
sb.Append(" case ");
int i = 0;
foreach (var item in loopItems)
{
int j = 0;
sb.Append(" when (");
foreach (var condition in item.Value)
{
sb.Append(condition.ColumnCode + condition.Operator + condition.ColumnCode1);
if (j != item.Value.Count - 1)
{
sb.Append(" AND ");
}
j++;
}
if (i != loopItems.Count() - 1)
{
sb.Append(") then '" + item.Key + "' ");
}
else
{
sb.Append(") then '" + item.Key + "' end as GroupName, ");
}
i++;
}
}
sb.Append(" * ");
sb.Append("FROM (");
sb.Append(" SELECT A.*, B.f267, B.f268, B.f269, B.f270, B.f271, B.f272, B.f273, B.f274, B.f275, B.f276,");
sb.Append(" C.f109, C.f164, C.f165, C.f166, C.f167, C.f168, C.f169, C.f170, C.f171, C.f172, C.f173,");
sb.Append(" D.f160, D.f174, D.f175, D.f176, D.f177, D.f178, D.f179, D.f180, D.f181, D.f182, D.f183");
sb.Append(" FROM (");
sb.Append(" SELECT rank_type, create_date, f1, f2, f12, f13, f14, f124, f3, f62, f66, f69, f72, f75, f78, f81, f84, f87");
sb.Append(" FROM StockRankings");
sb.Append(" WHERE rank_type = 'today'");
sb.Append(" ) A");
sb.Append(" JOIN (");
sb.Append(" SELECT f267, f268, f269, f270, f271, f272, f273, f274, f275, f276, rank_type, create_date, f12");
sb.Append(" FROM StockRankings");
sb.Append(" WHERE rank_type = '3_days'");
sb.Append(" ) B ON A.f12 = B.f12 AND A.create_date = B.create_date");
sb.Append(" JOIN (");
sb.Append(" SELECT f109, f164, f165, f166, f167, f168, f169, f170, f171, f172, f173, rank_type, create_date, f12");
sb.Append(" FROM StockRankings");
sb.Append(" WHERE rank_type = '5_days'");
sb.Append(" ) C ON A.f12 = C.f12 AND A.create_date = C.create_date");
sb.Append(" JOIN (");
sb.Append(" SELECT f160, f174, f175, f176, f177, f178, f179, f180, f181, f182, f183, rank_type, create_date, f12");
sb.Append(" FROM StockRankings");
sb.Append(" WHERE rank_type = '10_days'");
sb.Append(" ) D ON A.f12 = D.f12 AND A.create_date = D.create_date");
sb.Append(") AS T");
if (BaseControl.ConditionList.Count > 0)
{
sb.Append(" where (");
int i = 0;
foreach (var item in loopItems)
{
int j = 0;
sb.Append("(");
foreach (var condition in item.Value)
{
sb.Append(condition.ColumnCode + condition.Operator + condition.ColumnCode1);
if (j != item.Value.Count - 1)
{
sb.Append(" AND ");
}
j++;
}
if (i != loopItems.Count() - 1)
{
sb.Append(") OR ");
}
else
{
sb.Append(") ");
}
i++;
}
sb.Append(") AND create_date='" + m_lastUpdateTime.ToString("yyyy-MM-dd HH:mm:ss") + "'");
}
else
{
sb.Append(" where create_date='" + m_lastUpdateTime.ToString("yyyy-MM-dd HH:mm:ss") + "'");
}
var focus = BaseControl.TransCodeList.Where(t => t.Status == 1).ToList();
if (focus.Count > 0)
{
string codes = "";
for (int i = 0; i < focus.Count; i++)
{
codes += "'" + focus[i].Code + "'";
if (i != focus.Count - 1)
codes += ",";
}
sb.Append(" AND f12 in(" + codes + ")");
}
Logger.SQL("记录定时报警SQL语句:" + sb.ToString());
foreach (var item in BaseControl.TransCodeList)
{
item.TransExceptionTime = DateTime.MinValue;
item.Des = "";
item.WaringTypeGroup = "";
item.HXWarningGroup = "";
item.WaringType = "";
item.HXWarning = "";
}
var list = BaseControl.QueryAllDBData<StockRankingBase>(sb.ToString(), 1);
var dbClient = new TransDataDB("transData", DateTime.Now.ToString("yyyyMMdd"));
bool isChange = false;
string deleteSql = "delete from WaringTable where create_date<'" + DateTime.Now.AddDays(-2).ToString("yyyy-MM-dd HH:mm:ss") + "'";
dbClient.SaveDataToSQLliteDB(deleteSql);
Logger.SQL("删除SQL:" + deleteSql);
DateTime warningTime = DateTime.Now;
foreach (var item in list)// 单独处理横向预警
{
var result = BaseControl.WaringInfoList.FirstOrDefault(t => t.create_date == item.create_date && item.f12 == t.f12);
if (result == null)
{
isChange = true;
string txt = "请注意,代码为[" + item.f12 + "]名称为[" + item.f14 + "]\r\n在[" + item.create_date + "]出现疑似大额买单\r\n请尽快确认!";
ThreadPool.QueueUserWorkItem(t =>
{
var tips = new BottomRightPopup(txt);
tips.ShowDialog();
});
string insert = string.Format("INSERT INTO WaringTable ( create_date, f12, f14) VALUES( '{0}', '{1}', '{2}');", item.create_date, item.f12, item.f14);
dbClient.SaveDataToSQLliteDB(insert);
}
if (BaseControl.WaringInfoList.Count > 1000)
BaseControl.WaringInfoList.RemoveAt(0);
BaseControl.WaringInfoList.Add(new WaringInfo
{
create_date = item.create_date,
f12 = item.f12
});
var model = BaseControl.TransCodeList.FirstOrDefault(t => t.Code == item.f12);
if (model != null)
{
model.HXWarningGroup = item.GroupName;
model.TransExceptionTime = warningTime;
model.HXWarning = "横向预警";
BaseControl.TransCodeList.Remove(model);
BaseControl.TransCodeList.Add(model);
}
}
4. 软件运行截图
基础列表
预警设置
交易趋势变化
预警效果
有需要该软件的,可后台给我私信留言哦。