Advertisement
Guest User

Untitled

a guest
Apr 6th, 2016
96
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 24.23 KB | None | 0 0
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.Concurrent;
  4. using System.Globalization;
  5. using System.Linq;
  6. using System.Text;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. using csclientnet;
  10. using Marathon.Trading.BookOrderTracker.Common.Models;
  11.  
  12. namespace Marathon.Trading.BookOrderTracker.ServerCore.DAL
  13. {
  14. public class ItgDataAccess
  15. {
  16.  
  17. public bool IS_PROD = true;
  18.  
  19. private const int CLIENT_SYMBOL_LOAD = 1;
  20. private const int HEAVY_SYMBOL_LOAD = 4;
  21.  
  22. //private const string SIM_USERNAME = "bherbstsim";
  23. //private const string SIM_PASSWORD = "Herbst76";
  24.  
  25. private const string SIM_USERNAME = "marathsim";
  26. private const string SIM_PASSWORD = "Marath37";
  27.  
  28. //private const string SIM_USERNAME = "sethsim1";
  29. //private const string SIM_PASSWORD = "Seth3s#m";
  30.  
  31. private const string PROD_USERNAME = "mthndata";
  32. private const string PROD_PASSWORD = "mAt63#da";
  33.  
  34. private int _disconnectResetPeriod;
  35. public int DisconnectThreshold {get; set;}
  36. public int DisconnectResetPeriod
  37. {
  38. get { return _disconnectResetPeriod; }
  39. set { if (_disconnectResetPeriod != value) { _disconnectResetPeriod = value; if(_disconnectResetTimer!=null) _disconnectResetTimer.Change(0, _disconnectResetPeriod);} }
  40. }
  41.  
  42.  
  43. private const string SIM_SERVER = "tcp=sim-csp.itg.com:40003";
  44. private const string CROSS_SERVER = "tcp=199.99.96.201:40003";
  45. private const string PROD_SERVER = "tcp=csp-ny.itg.com:30115";
  46. private const String SIM_PART = "TEST";
  47. private const String PROD_PART = "PROD";
  48. public delegate void ItgConnectionUpdate(String msg);
  49. public event ItgConnectionUpdate ItgDisconnected;
  50. public event ItgConnectionUpdate ItgReconnected;
  51. public event ItgConnectionUpdate ItgError;
  52.  
  53.  
  54. private string SERVER;
  55. private string PART;
  56. private string USERNAME;
  57. private string PASSWORD;
  58.  
  59. private BlockingCollection<ClientUpdate> _itgUpdateQueue;
  60. public static Task _itgProcessTask;
  61.  
  62. //Allow each instance of a client to hold up to CLIENT_SYMBOL_LOAD, each normal symbol has a load value of 4, heavy symbols have a load value of HEAVY_SYMBOL_LOAD
  63. private ConcurrentDictionary<int, Pair<int, Client>> _clientDictionary;
  64. private ConcurrentDictionary<int, Pair<int, ClientSession>> _sessionDictionary;
  65. private ConcurrentDictionary<string, int> _disconnectCount; //tracks the number of disconnects for each symbol
  66. private ConcurrentDictionary<string, int> _symbolToClientIdMap; //Identifies the id of the client where the symbol is subcribed
  67. private int _largestClientId;
  68.  
  69. private HashSet<string> _heavySymbolOverrides = new HashSet<string>() { "SPY", "VXX" };
  70.  
  71. private Client stockListener;
  72. private ClientSession stockSession;
  73.  
  74. private Logger _logger;
  75.  
  76. private Timer _disconnectResetTimer;
  77.  
  78. public Task updateTask;
  79.  
  80. private Action<Tuple<string, List<OptionOrderUpdate>>> _optionOrderUpdateDelegate;
  81. private Action<Underlyer> _underlyerUpdateDelegate;
  82.  
  83. private CancellationTokenSource cancellationToken = new CancellationTokenSource();
  84.  
  85. public delegate void MessageHandler(string message);
  86. public event MessageHandler MessageReceived;
  87. public delegate void DisconnectedHandler(string symbol);
  88. public event DisconnectedHandler SymbolDisconnectedEvent;
  89.  
  90. public ItgDataAccess(Action<Tuple<string, List<OptionOrderUpdate>>> optionOrderUpdateDelegate, Action<Underlyer> underlyerUpdateDelegate, Logger logger)
  91. {
  92. if (IS_PROD)
  93. {
  94. SERVER = PROD_SERVER;
  95. PART = PROD_PART;
  96. USERNAME = PROD_USERNAME;
  97. PASSWORD = PROD_PASSWORD;
  98. }
  99. else
  100. {
  101. SERVER = CROSS_SERVER;
  102. PART = SIM_PART;
  103. USERNAME = SIM_USERNAME;
  104. PASSWORD = SIM_PASSWORD;
  105. }
  106.  
  107.  
  108. DisconnectThreshold = 10;
  109. DisconnectResetPeriod = 60000;
  110.  
  111. _clientDictionary = new ConcurrentDictionary<int, Pair<int, Client>>();
  112. _sessionDictionary = new ConcurrentDictionary<int, Pair<int, ClientSession>>();
  113. _disconnectCount = new ConcurrentDictionary<string, int>();
  114. _symbolToClientIdMap = new ConcurrentDictionary<string, int>();
  115. _largestClientId = 0;
  116.  
  117.  
  118. _itgUpdateQueue = new BlockingCollection<ClientUpdate>();
  119.  
  120. _logger = logger;
  121. _optionOrderUpdateDelegate = optionOrderUpdateDelegate;
  122. _underlyerUpdateDelegate = underlyerUpdateDelegate;
  123. Console.WriteLine("ITG connection established.");
  124.  
  125. var stockTuple = SetupSession();
  126. stockListener = stockTuple.Item1;
  127. stockSession = stockTuple.Item2;
  128.  
  129. _disconnectResetTimer = new Timer((sender) =>
  130. {
  131. foreach(string key in _disconnectCount.Keys.ToList())
  132. _disconnectCount[key] = 0;
  133. });
  134. _disconnectResetTimer.Change(0, DisconnectResetPeriod);
  135.  
  136. ProcessItgUpdates();
  137. }
  138.  
  139. private Tuple<Client, ClientSession> SetupSession()
  140. {
  141. var _session = new ClientSession();
  142. var _listener = new Client();
  143.  
  144. _listener.SubscribeStatus += (sender, status) =>
  145. {
  146. var message = String.Format("Listener subscribed to {0}. Status is {1}.", status.Subscription, status.Status);
  147. _logger.QueueLine(message);
  148. var messageReceived = MessageReceived;
  149. if(messageReceived != null)
  150. messageReceived(message);
  151. };
  152.  
  153. _listener.Disconnect += (s, e) =>
  154. {
  155. if (ItgDisconnected != null)
  156. ItgDisconnected(e.Reason.ToString());
  157. string symbol = "";
  158. int clientDictId = -1;
  159. foreach (KeyValuePair<int, Pair<int, Client>> kvp in _clientDictionary)
  160. {
  161. if (s == kvp.Value.Item2)
  162. clientDictId = kvp.Key;
  163. }
  164.  
  165. var match = _symbolToClientIdMap.Where(kvp => kvp.Value == clientDictId);
  166.  
  167. if (match != null && match.Count() > 0)
  168. {
  169. symbol = match.ElementAt(0).Key;
  170. var message = symbol + " - ITG feed disconnected: " + e.Reason.ToString();
  171. _logger.QueueLine(message);
  172. var messageReceived = MessageReceived;
  173. if (messageReceived != null)
  174. {
  175. messageReceived("Pre-Disconnect Update Buildup: " + _itgUpdateQueue.LongCount());
  176. messageReceived(message);
  177. }
  178.  
  179. if (!symbol.Equals("") && _disconnectCount.ContainsKey(symbol))
  180. {
  181. _disconnectCount[symbol]++;
  182. if (_disconnectCount[symbol] > DisconnectThreshold)
  183. {
  184. UnsubscribeFromSymbol(symbol);
  185. if (messageReceived != null)
  186. messageReceived("Too many disconnects. Disconnected " + symbol);
  187. SymbolDisconnectedEvent(symbol);
  188. }
  189. }
  190. }
  191.  
  192.  
  193. };
  194.  
  195. _listener.Error += (s, e) =>
  196. {
  197. var message = "ITG encountered an error: " + e.Description + " Status: " + e.Status;
  198. _logger.QueueLine(message);
  199. var messageReceived = MessageReceived;
  200. if(messageReceived!=null)
  201. messageReceived(message);
  202. if (ItgError != null)
  203. ItgError(e.Description);
  204. };
  205.  
  206. _listener.Reconnect += (s) =>
  207. {
  208. var message = "ITG feed automatically reconnected.";
  209. _logger.QueueLine(message);
  210. var messageReceived = MessageReceived;
  211. if(messageReceived != null)
  212. messageReceived(message);
  213. if (ItgReconnected != null)
  214. ItgReconnected("Reconnected");
  215. };
  216.  
  217. _listener.Update += OnItgDataUpdate;
  218.  
  219. _listener.SetClientSession(_session);
  220.  
  221. _session.Connect(SERVER, USERNAME, PASSWORD);
  222. return new Tuple<Client, ClientSession>(_listener, _session);
  223. }
  224.  
  225. private void OnItgDataUpdate(Client sender, ClientUpdate update)
  226. {
  227. _itgUpdateQueue.Add(update);
  228. }
  229.  
  230. private void ProcessItgUpdates()
  231. {
  232. _itgProcessTask = Task.Factory.StartNew(() =>
  233. {
  234. foreach(var update in _itgUpdateQueue.GetConsumingEnumerable())
  235. {
  236. if (cancellationToken.IsCancellationRequested)
  237. return;
  238.  
  239. string[] subscriptionStrings;
  240. subscriptionStrings = update.Subscription.Split('.');
  241. UpdateOrderData(subscriptionStrings, update.Update);
  242. update.Dispose();
  243. }
  244. });
  245. }
  246.  
  247.  
  248.  
  249. private void UpdateOrderData(string[] subscriptionStrings, Blob message)
  250. {
  251.  
  252. DateTime timestamp = DateTime.Now;;
  253.  
  254. if (subscriptionStrings[0].Equals(pricesrvfields.PRICESRV_SUB_STOCK))
  255. {
  256. string symbol = subscriptionStrings[subscriptionStrings.Length - 1];
  257.  
  258. Underlyer underlyer = UpdateStockData(symbol, message);
  259. if(underlyer != null)
  260. _underlyerUpdateDelegate(underlyer);
  261.  
  262. }
  263. else if (subscriptionStrings[0].Equals(pricesrvfields.PRICESRV_SUB_OPTION_SERIES) || subscriptionStrings[0].Equals(pricesrvfields.PRICESRV_SUB_OPTION_MONTH)
  264. || subscriptionStrings[0].Equals(pricesrvfields.PRICESRV_SUB_OPTION_SERIES_DELAYED))
  265. {
  266. string osiCode = "";
  267. var bboUpdates = new List<OptionOrderUpdate>();
  268.  
  269. for (int i = 0; i < message.FieldCount; i++)
  270. {
  271. string tempOsiCode = UpdateOptionData(timestamp, bboUpdates, message, i);
  272. if (osiCode.Equals("") && !tempOsiCode.Equals(""))
  273. osiCode = tempOsiCode;
  274. }
  275.  
  276. if (bboUpdates.Count > 0)
  277. _optionOrderUpdateDelegate(Tuple.Create(osiCode, bboUpdates));
  278.  
  279. }
  280.  
  281. message.Dispose();
  282. }
  283.  
  284.  
  285. private Underlyer UpdateStockData(string symbol, Blob message)
  286. {
  287. double lastStockPrice = 0, stockBid = 0, stockAsk = 0;
  288. for (int i = 0; i < message.FieldCount; i++)
  289. {
  290. switch (message.GetFieldDescription(i))
  291. {
  292. //Stock price update
  293. case pricesrvfields.FLD_LAST:
  294. lastStockPrice = message.GetDouble(i);
  295. break;
  296. case pricesrvfields.FLD_BID:
  297. stockBid = message.GetDouble(i);
  298. break;
  299. case pricesrvfields.FLD_ASK:
  300. stockAsk = message.GetDouble(i);
  301. break;
  302. }
  303. }
  304. if (lastStockPrice != 0 || stockBid != 0 || stockAsk != 0)
  305. return new Underlyer(symbol, lastStockPrice, stockBid, stockAsk);
  306. else
  307. return null;
  308. }
  309.  
  310. private string UpdateOptionData(DateTime timestamp, List<OptionOrderUpdate> bboUpdates, Blob message, int fieldNum)
  311. {
  312. string osiCode="";
  313. OptionOrderUpdate update;
  314. switch (message.GetFieldDescription(fieldNum))
  315. {
  316. //Option Data
  317. case pricesrvfields.FLD_OPRACODE:
  318. osiCode = message.GetString(fieldNum);
  319. break;
  320. case pricesrvfields.FLD_EXCH_AMEX:
  321. update = GetExchangeData(message.GetBlob(fieldNum), "AMEX");
  322. if (update.Bid >= 0 || update.BidSize >= 0 || update.Ask >= 0 || update.AskSize >= 0)
  323. bboUpdates.Add(update);
  324. break;
  325. case pricesrvfields.FLD_EXCH_BOX:
  326. update = GetExchangeData(message.GetBlob(fieldNum), "BOX");
  327. if (update.Bid >= 0 || update.BidSize >= 0 || update.Ask >= 0 || update.AskSize >= 0)
  328. bboUpdates.Add(update);
  329. break;
  330. case pricesrvfields.FLD_EXCH_CBOE:
  331. update = GetExchangeData(message.GetBlob(fieldNum), "CBOE");
  332. if (update.Bid >= 0 || update.BidSize >= 0 || update.Ask >= 0 || update.AskSize >= 0)
  333. bboUpdates.Add(update);
  334. break;
  335. case pricesrvfields.FLD_EXCH_ISE:
  336. update = GetExchangeData(message.GetBlob(fieldNum), "ISE");
  337. if (update.Bid >= 0 || update.BidSize >= 0 || update.Ask >= 0 || update.AskSize >= 0)
  338. bboUpdates.Add(update);
  339. break;
  340. case pricesrvfields.FLD_EXCH_PSE:
  341. update = GetExchangeData(message.GetBlob(fieldNum), "ARCA");
  342. if (update.Bid > 0 || update.BidSize > 0 || update.Ask > 0 || update.AskSize > 0)
  343. bboUpdates.Add(update);
  344. break;
  345. case pricesrvfields.FLD_EXCH_PHLX:
  346. update = GetExchangeData(message.GetBlob(fieldNum), "PHLX");
  347. if (update.Bid >= 0 || update.BidSize >= 0 || update.Ask >= 0 || update.AskSize >= 0)
  348. bboUpdates.Add(update);
  349. break;
  350. case pricesrvfields.FLD_EXCH_NSDQ:
  351. update = GetExchangeData(message.GetBlob(fieldNum), "NSDQ");
  352. if (update.Bid >= 0 || update.BidSize >= 0 || update.Ask >= 0 || update.AskSize >= 0)
  353. bboUpdates.Add(update);
  354. break;
  355. case pricesrvfields.FLD_EXCH_BATS:
  356. update = GetExchangeData(message.GetBlob(fieldNum), "BATS");
  357. if (update.Bid >= 0 || update.BidSize >= 0 || update.Ask >= 0 || update.AskSize >= 0)
  358. bboUpdates.Add(update);
  359. break;
  360. case pricesrvfields.FLD_EXCH_C2OX:
  361. update = GetExchangeData(message.GetBlob(fieldNum), "C2OX");
  362. if (update.Bid >= 0 || update.BidSize >= 0 || update.Ask >= 0 || update.AskSize >= 0)
  363. bboUpdates.Add(update);
  364. break;
  365. case pricesrvfields.FLD_EXCH_BXO:
  366. update = GetExchangeData(message.GetBlob(fieldNum), "BXO");
  367. if (update.Bid >= 0 || update.BidSize >= 0 || update.Ask >= 0 || update.AskSize >= 0)
  368. bboUpdates.Add(update);
  369. break;
  370. case pricesrvfields.FLD_EXCH_MX:
  371. update = GetExchangeData(message.GetBlob(fieldNum), "MX");
  372. if (update.Bid >= 0 || update.BidSize >= 0 || update.Ask >= 0 || update.AskSize >= 0)
  373. bboUpdates.Add(update);
  374. break;
  375. case pricesrvfields.FLD_EXCH_MIO:
  376. update = GetExchangeData(message.GetBlob(fieldNum), "MIAX");
  377. if (update.Bid >= 0 || update.BidSize >= 0 || update.Ask >= 0 || update.AskSize >= 0)
  378. bboUpdates.Add(update);
  379. break;
  380. case pricesrvfields.FLD_EXCH_GMNI:
  381. update = GetExchangeData(message.GetBlob(fieldNum), "GMNI");
  382. if (update.Bid >= 0 || update.BidSize >= 0 || update.Ask >= 0 || update.AskSize >= 0)
  383. bboUpdates.Add(update);
  384. break;
  385. case pricesrvfields.FLD_EXCH_EDGE:
  386. update = GetExchangeData(message.GetBlob(fieldNum), "EDGX");
  387. if (update.Bid >= 0 || update.BidSize >= 0 || update.Ask >= 0 || update.AskSize >= 0)
  388. bboUpdates.Add(update);
  389. break;
  390. }
  391. return osiCode;
  392. }
  393.  
  394.  
  395. private OptionOrderUpdate GetExchangeData(Blob message, string exchange)
  396. {
  397. double bid = -1, ask = -1;
  398. int bidSize = -1, askSize = -1;
  399. for (int i = 0; i < message.FieldCount; i++)
  400. {
  401. switch (message.GetFieldDescription(i))
  402. {
  403. case pricesrvfields.FLD_BID:
  404. bid = message.GetDouble(i);
  405. break;
  406. case pricesrvfields.FLD_ASK:
  407. ask = message.GetDouble(i);
  408. break;
  409. case pricesrvfields.FLD_BIDSIZE:
  410. bidSize = message.GetInt(i);
  411. break;
  412. case pricesrvfields.FLD_ASKSIZE:
  413. askSize = message.GetInt(i);
  414. break;
  415. }
  416. }
  417. message.Dispose();
  418. return new OptionOrderUpdate(bid, ask, bidSize, askSize, exchange);
  419. }
  420.  
  421. public void SubscribeToSymbol(String symbol)
  422. {
  423. //If the string is nothing or we have already subscribed to it, don't do it again
  424. if (String.IsNullOrEmpty(symbol) || _symbolToClientIdMap.ContainsKey(symbol))
  425. return;
  426.  
  427. Client optionListener;
  428. ClientSession optionSession;
  429. string subscriptionStr;
  430. int clientId = -1;
  431.  
  432. int symbolLoad = (_heavySymbolOverrides.Contains(symbol)) ? HEAVY_SYMBOL_LOAD : 1;
  433.  
  434. foreach(KeyValuePair<int, Pair<int, Client>> kvp in _clientDictionary)
  435. {
  436. if (kvp.Value.Item1 + symbolLoad <= CLIENT_SYMBOL_LOAD)
  437. {
  438. clientId = kvp.Key;
  439. break;
  440. }
  441.  
  442. }
  443.  
  444. if (clientId == -1)
  445. {
  446. var optionTuple = SetupSession();
  447. optionListener = optionTuple.Item1;
  448. optionSession = optionTuple.Item2;
  449. _clientDictionary.TryAdd(_largestClientId, new Pair<int, Client>(0, optionListener));
  450. _sessionDictionary.TryAdd(_largestClientId, new Pair<int, ClientSession>(0, optionSession));
  451. clientId = _largestClientId;
  452. _largestClientId++;
  453. }
  454. else
  455. {
  456. optionListener = _clientDictionary[clientId].Item2;
  457. optionSession = _sessionDictionary[clientId].Item2;
  458. }
  459.  
  460. subscriptionStr = pricesrvfields.PRICESRV_SUB_OPTION_SERIES_DELAYED + "." + PART + "." + symbol.ToUpper();
  461. optionListener.Subscribe(pricesrvfields.SERVICEID_PRICE, subscriptionStr, true);
  462. _symbolToClientIdMap.TryAdd(symbol, clientId);
  463. _clientDictionary[clientId].Item1 += symbolLoad;
  464. _sessionDictionary[clientId].Item1 += symbolLoad;
  465.  
  466. //Subscribe to stock price for the symbol
  467. subscriptionStr = pricesrvfields.PRICESRV_SUB_STOCK + "." + PART + ". ." + symbol.ToUpper();
  468. stockListener.Subscribe(pricesrvfields.SERVICEID_PRICE, subscriptionStr, true);
  469. _disconnectCount.TryAdd(symbol, 0);
  470. _logger.QueueLine("Subscribed to: " + symbol);
  471.  
  472. }
  473.  
  474. public void UnsubscribeFromSymbol(String symbol)
  475. {
  476. if (String.IsNullOrEmpty(symbol) || !_symbolToClientIdMap.ContainsKey(symbol))
  477. return;
  478.  
  479. string subscriptionStr;
  480. try
  481. {
  482. int symbolLoad = (_heavySymbolOverrides.Contains(symbol)) ? HEAVY_SYMBOL_LOAD : 1;
  483.  
  484. //Unsubcribe from the option chain
  485. subscriptionStr = pricesrvfields.PRICESRV_SUB_OPTION_SERIES_DELAYED + "." + PART + "." + symbol.ToUpper();
  486.  
  487. int clientId = -1;
  488. if (_symbolToClientIdMap.TryGetValue(symbol, out clientId))
  489. {
  490. Pair<int, Client> optionClientPair;
  491. if (_clientDictionary.TryGetValue(clientId, out optionClientPair))
  492. {
  493. optionClientPair.Item2.Unsubscribe(pricesrvfields.SERVICEID_PRICE, subscriptionStr);
  494. optionClientPair.Item1 -= symbolLoad;
  495. }
  496.  
  497. Pair<int, ClientSession> optionSessionPair;
  498. if (_sessionDictionary.TryGetValue(clientId, out optionSessionPair))
  499. optionSessionPair.Item1 -= symbolLoad;
  500.  
  501. ///Unsubscribe from the stock price
  502. subscriptionStr = pricesrvfields.PRICESRV_SUB_STOCK + "." + PART + ". ." + symbol.ToUpper();
  503. stockListener.Unsubscribe(pricesrvfields.SERVICEID_PRICE, subscriptionStr);
  504.  
  505. int count;
  506. _disconnectCount.TryRemove(symbol, out count);
  507. var messageReceived = MessageReceived;
  508. if (messageReceived != null)
  509. messageReceived("Unsubscribing from: " + symbol);
  510. _logger.QueueLine("Unsubcribing from: " + symbol);
  511. }
  512.  
  513. }
  514. catch (Exception)
  515. {
  516. var messageReceived = MessageReceived;
  517. if(messageReceived != null)
  518. messageReceived("Error unsubscribing from: " + symbol);
  519. _logger.QueueLine("Error unsubscribing from: " + symbol);
  520. }
  521. }
  522.  
  523. private void UnsubscribeAllAndDisconnect()
  524. {
  525. foreach (KeyValuePair<int, Pair<int, Client>> kvp in _clientDictionary)
  526. {
  527. kvp.Value.Item2.CancelAllSubscriptions();
  528. kvp.Value.Item1 = 0;
  529. }
  530. stockListener.CancelAllSubscriptions();
  531.  
  532. try
  533. {
  534. foreach (KeyValuePair<int, Pair<int, ClientSession>> kvp in _sessionDictionary)
  535. {
  536. var optionSession = kvp.Value.Item2;
  537. if(optionSession.Connected)
  538. optionSession.Disconnect();
  539. optionSession.Dispose();
  540. }
  541. if(stockSession.Connected)
  542. stockSession.Disconnect();
  543. stockSession.Dispose();
  544. }
  545. catch (Exception) { }
  546.  
  547. }
  548.  
  549. public void Shutdown()
  550. {
  551. UnsubscribeAllAndDisconnect();
  552. cancellationToken.Cancel();
  553. }
  554.  
  555. private string[] GetFrontMonths()
  556. {
  557. string[] frontMonths = new string[4];
  558. DateTime month0, month1, month2, month3;
  559.  
  560. month0 = DateTime.Now;
  561. month1 = DateTime.Now.AddMonths(1);
  562. month2 = DateTime.Now.AddMonths(2);
  563. month3 = DateTime.Now.AddMonths(3);
  564.  
  565. frontMonths[0] = month0.ToString("yyyyMM");
  566. frontMonths[1] = month1.ToString("yyyyMM");
  567. frontMonths[2] = month2.ToString("yyyyMM");
  568. frontMonths[3] = month3.ToString("yyyyMM");
  569.  
  570. return frontMonths;
  571. }
  572.  
  573. }
  574.  
  575. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement